@whittlelabs/sifter 0.30.9 → 0.30.11

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.
Files changed (4) hide show
  1. package/README.md +1 -0
  2. package/bin.js +433 -120
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/bin.js CHANGED
@@ -11199,16 +11199,260 @@ var require_client = __commonJS({
11199
11199
  }
11200
11200
  });
11201
11201
 
11202
+ // ../../packages/keep/dist/rate-limited.js
11203
+ var require_rate_limited = __commonJS({
11204
+ "../../packages/keep/dist/rate-limited.js"(exports2) {
11205
+ "use strict";
11206
+ Object.defineProperty(exports2, "__esModule", { value: true });
11207
+ exports2.RateLimitGate = exports2.RATE_LIMIT_DEFAULT_MS = exports2.RATE_LIMIT_CAP_MS = exports2.RATE_LIMIT_FLOOR_MS = exports2.KeepRateLimitedError = void 0;
11208
+ exports2.retryAfterMsOf = retryAfterMsOf;
11209
+ var KeepRateLimitedError = class extends Error {
11210
+ code = "RATE_LIMITED";
11211
+ /** Host the exchange was sent to. */
11212
+ host;
11213
+ /** Keep's own words for the refusal ("Too many attempts. Please try again later."). */
11214
+ reason;
11215
+ /** How long from now to wait before asking again (ms), already floored and capped. */
11216
+ retryAfterMs;
11217
+ /** When the next exchange may be attempted (epoch ms). */
11218
+ retryAt;
11219
+ constructor(message, detail) {
11220
+ super(message);
11221
+ this.name = "KeepRateLimitedError";
11222
+ this.host = detail.host;
11223
+ this.reason = detail.reason;
11224
+ this.retryAt = detail.retryAt;
11225
+ this.retryAfterMs = Math.max(0, detail.retryAt - Date.now());
11226
+ }
11227
+ };
11228
+ exports2.KeepRateLimitedError = KeepRateLimitedError;
11229
+ exports2.RATE_LIMIT_FLOOR_MS = 5e3;
11230
+ exports2.RATE_LIMIT_CAP_MS = 15 * 6e4;
11231
+ exports2.RATE_LIMIT_DEFAULT_MS = 6e4;
11232
+ function retryAfterMsOf(body, headers, now = Date.now()) {
11233
+ return clamp(bodyRetryAfter(body) ?? headerRetryAfter(headers, now) ?? exports2.RATE_LIMIT_DEFAULT_MS);
11234
+ }
11235
+ function bodyRetryAfter(body) {
11236
+ const value = body?.error?.retryAfter;
11237
+ return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : null;
11238
+ }
11239
+ function headerRetryAfter(headers, now) {
11240
+ const raw = headers?.get("retry-after")?.trim();
11241
+ if (!raw)
11242
+ return null;
11243
+ if (/^\d+$/.test(raw))
11244
+ return Number(raw) * 1e3;
11245
+ const at = Date.parse(raw);
11246
+ return Number.isNaN(at) ? null : at - now;
11247
+ }
11248
+ function clamp(ms) {
11249
+ return Math.min(exports2.RATE_LIMIT_CAP_MS, Math.max(exports2.RATE_LIMIT_FLOOR_MS, ms));
11250
+ }
11251
+ var RateLimitGate = class {
11252
+ closed = null;
11253
+ /** Record a 429 and return the error to throw for it. */
11254
+ close(detail) {
11255
+ const retryAt = Date.now() + detail.retryAfterMs;
11256
+ const when = new Date(retryAt).toISOString().replace(/\.\d{3}Z$/, "Z");
11257
+ this.closed = new KeepRateLimitedError(`[${detail.manager}] Keep at ${detail.host} is rate limiting this client's token exchange (429: ${detail.reason}); not asking again before ${when}`, { host: detail.host, reason: detail.reason, retryAt });
11258
+ return this.closed;
11259
+ }
11260
+ /** Throws the rate-limited error while the gate is closed; a no-op once the time has passed. */
11261
+ check() {
11262
+ const closed = this.closed;
11263
+ if (closed === null)
11264
+ return;
11265
+ if (Date.now() >= closed.retryAt) {
11266
+ this.closed = null;
11267
+ return;
11268
+ }
11269
+ throw new KeepRateLimitedError(closed.message, {
11270
+ host: closed.host,
11271
+ reason: closed.reason,
11272
+ retryAt: closed.retryAt
11273
+ });
11274
+ }
11275
+ };
11276
+ exports2.RateLimitGate = RateLimitGate;
11277
+ }
11278
+ });
11279
+
11280
+ // ../../packages/keep/dist/exchange.js
11281
+ var require_exchange = __commonJS({
11282
+ "../../packages/keep/dist/exchange.js"(exports2) {
11283
+ "use strict";
11284
+ Object.defineProperty(exports2, "__esModule", { value: true });
11285
+ exports2.KeepUnavailableError = void 0;
11286
+ exports2.exchangeCredentials = exchangeCredentials;
11287
+ exports2.hostOf = hostOf;
11288
+ var rate_limited_1 = require_rate_limited();
11289
+ var KeepUnavailableError = class extends Error {
11290
+ code = "SERVICE_UNAVAILABLE";
11291
+ /** Host the exchange was sent to, so a log line names the service that is down. */
11292
+ host;
11293
+ /** The status whatever answered sent, or null when nothing answered at all. */
11294
+ statusCode;
11295
+ constructor(message, detail) {
11296
+ super(message, detail.cause !== void 0 ? { cause: detail.cause } : void 0);
11297
+ this.name = "KeepUnavailableError";
11298
+ this.host = detail.host;
11299
+ this.statusCode = detail.statusCode;
11300
+ }
11301
+ };
11302
+ exports2.KeepUnavailableError = KeepUnavailableError;
11303
+ async function exchangeCredentials(request) {
11304
+ const { manager, url } = request;
11305
+ const host = hostOf(url);
11306
+ let response;
11307
+ try {
11308
+ response = await fetch(url, {
11309
+ method: "POST",
11310
+ headers: { "Content-Type": "application/json" },
11311
+ body: JSON.stringify({
11312
+ clientId: request.clientId,
11313
+ clientSecret: request.clientSecret
11314
+ })
11315
+ });
11316
+ } catch (err) {
11317
+ throw new KeepUnavailableError(`[${manager}] could not reach Keep at ${host}: ${describeTransportError(err)}`, { host, statusCode: null, cause: err });
11318
+ }
11319
+ request.onResponse?.(response);
11320
+ const text = await response.text().catch(() => "");
11321
+ let json;
11322
+ try {
11323
+ json = JSON.parse(text);
11324
+ } catch {
11325
+ json = null;
11326
+ }
11327
+ if (response.status >= 500) {
11328
+ const reason = json?.error?.message ?? `${request.exchangeName} failed with status ${response.status}`;
11329
+ throw new KeepUnavailableError(`[${manager}] Keep at ${host} is unavailable (${response.status}): ${reason}`, { host, statusCode: response.status });
11330
+ }
11331
+ if (response.status === 429) {
11332
+ throw request.rateLimit.close({
11333
+ manager,
11334
+ host,
11335
+ reason: json?.error?.message ?? "Too Many Requests",
11336
+ retryAfterMs: (0, rate_limited_1.retryAfterMsOf)(json, response.headers)
11337
+ });
11338
+ }
11339
+ if (json === null) {
11340
+ throw new Error(`[${manager}] Keep at ${host} answered ${response.status} with a body that is not JSON`);
11341
+ }
11342
+ if (!response.ok || !json.success) {
11343
+ const msg = json.error?.message ?? `${request.exchangeName} failed with status ${response.status}`;
11344
+ if (response.status === 401 && request.refused) {
11345
+ throw request.refused(msg);
11346
+ }
11347
+ throw new Error(`[${manager}] ${msg}`);
11348
+ }
11349
+ return json.data;
11350
+ }
11351
+ function hostOf(url) {
11352
+ try {
11353
+ return new URL(url).host;
11354
+ } catch {
11355
+ return url;
11356
+ }
11357
+ }
11358
+ function describeTransportError(err) {
11359
+ if (err instanceof Error) {
11360
+ const cause = err.cause;
11361
+ if (cause instanceof Error && cause.message && cause.message !== err.message) {
11362
+ return `${err.message}: ${cause.message}`;
11363
+ }
11364
+ return err.message;
11365
+ }
11366
+ return String(err);
11367
+ }
11368
+ }
11369
+ });
11370
+
11371
+ // ../../packages/keep/dist/wait-for-token.js
11372
+ var require_wait_for_token = __commonJS({
11373
+ "../../packages/keep/dist/wait-for-token.js"(exports2) {
11374
+ "use strict";
11375
+ Object.defineProperty(exports2, "__esModule", { value: true });
11376
+ exports2.waitForKeepToken = waitForKeepToken;
11377
+ var WAIT_FOR_TOKEN_MAX_MS = 20 * 6e4;
11378
+ var INITIAL_DELAY_MS = 1e3;
11379
+ var MAX_DELAY_MS = 3e4;
11380
+ async function waitForKeepToken(getToken, options) {
11381
+ const logger = options.logger ?? console;
11382
+ const initialDelay = options.initialDelayMs ?? INITIAL_DELAY_MS;
11383
+ const maxDelay = options.maxDelayMs ?? MAX_DELAY_MS;
11384
+ const started = Date.now();
11385
+ const deadline = started + (options.maxWaitMs ?? WAIT_FOR_TOKEN_MAX_MS);
11386
+ const tag = `[${options.label}]`;
11387
+ let delay = initialDelay;
11388
+ let waiting = null;
11389
+ for (; ; ) {
11390
+ let pause;
11391
+ try {
11392
+ const token = await getToken();
11393
+ if (waiting !== null) {
11394
+ logger.info(`${tag} Keep issued a token after ${formatDuration(Date.now() - started)} of waiting`);
11395
+ }
11396
+ return token;
11397
+ } catch (err) {
11398
+ if (isRateLimited(err)) {
11399
+ pause = Math.max(0, err.retryAfterMs);
11400
+ delay = initialDelay;
11401
+ if (waiting !== "rate-limited") {
11402
+ waiting = "rate-limited";
11403
+ logger.warn(`${tag} Keep is rate limiting this client's token exchange (${err.message}). The credentials are not in question; waiting ${formatDuration(pause)} to ask again.`);
11404
+ }
11405
+ } else if (isUnavailable(err)) {
11406
+ pause = delay;
11407
+ delay = Math.min(delay * 2, maxDelay);
11408
+ if (waiting !== "unavailable") {
11409
+ waiting = "unavailable";
11410
+ logger.warn(`${tag} Keep is unavailable (${err.message}); retrying with backoff.`);
11411
+ }
11412
+ } else {
11413
+ throw err;
11414
+ }
11415
+ if (Date.now() + pause > deadline) {
11416
+ logger.error(`${tag} giving up on a Keep token after ${formatDuration(Date.now() - started)}: ${err.message}`);
11417
+ throw err;
11418
+ }
11419
+ }
11420
+ await new Promise((resolve) => setTimeout(resolve, pause));
11421
+ }
11422
+ }
11423
+ function isRateLimited(err) {
11424
+ const e = err;
11425
+ return e?.code === "RATE_LIMITED" && typeof e.retryAfterMs === "number";
11426
+ }
11427
+ function isUnavailable(err) {
11428
+ return err?.code === "SERVICE_UNAVAILABLE";
11429
+ }
11430
+ function formatDuration(ms) {
11431
+ const s = Math.round(ms / 1e3);
11432
+ if (s < 60)
11433
+ return `${s}s`;
11434
+ const m = Math.floor(s / 60);
11435
+ const rest = s % 60;
11436
+ return rest === 0 ? `${m}m` : `${m}m${rest}s`;
11437
+ }
11438
+ }
11439
+ });
11440
+
11202
11441
  // ../../packages/keep/dist/service-token.js
11203
11442
  var require_service_token = __commonJS({
11204
11443
  "../../packages/keep/dist/service-token.js"(exports2) {
11205
11444
  "use strict";
11206
11445
  Object.defineProperty(exports2, "__esModule", { value: true });
11207
11446
  exports2.ServiceTokenManager = void 0;
11447
+ var rate_limited_1 = require_rate_limited();
11448
+ var exchange_1 = require_exchange();
11449
+ var wait_for_token_1 = require_wait_for_token();
11208
11450
  var ServiceTokenManager = class {
11209
11451
  token = null;
11210
11452
  expiresAt = 0;
11211
11453
  pendingExchange = null;
11454
+ /** Closed by a 429 until Keep's retry-after: no exchange is sent before it. */
11455
+ rateLimit = new rate_limited_1.RateLimitGate();
11212
11456
  keepApiUrl;
11213
11457
  clientId;
11214
11458
  clientSecret;
@@ -11227,6 +11471,14 @@ var require_service_token = __commonJS({
11227
11471
  /**
11228
11472
  * Returns a valid service token, refreshing if needed.
11229
11473
  * Returns null if Keep credentials are not configured (graceful degradation for local dev).
11474
+ *
11475
+ * Throws `KeepUnavailableError` when Keep did not answer or answered 5xx,
11476
+ * `KeepRateLimitedError` when Keep's exchange limiter locked this client out
11477
+ * (and, until the time Keep named, without asking again), and a plain `Error`
11478
+ * for any other refusal, a 401 above all. A refresh that fails for either
11479
+ * of the first two reasons while the cached token has not yet expired
11480
+ * answers with the cached token instead: the refresh starts 30s early
11481
+ * precisely so a failed one costs nothing.
11230
11482
  */
11231
11483
  async getToken() {
11232
11484
  if (!this.enabled)
@@ -11237,35 +11489,51 @@ var require_service_token = __commonJS({
11237
11489
  if (this.pendingExchange) {
11238
11490
  return this.pendingExchange;
11239
11491
  }
11240
- this.pendingExchange = this.exchange();
11492
+ this.pendingExchange = this.refresh();
11241
11493
  try {
11242
11494
  return await this.pendingExchange;
11243
11495
  } finally {
11244
11496
  this.pendingExchange = null;
11245
11497
  }
11246
11498
  }
11499
+ /**
11500
+ * `getToken`, for a service that cannot start without a token: waits out a
11501
+ * rate limit (for the time Keep named) and an outage (with backoff), bounded
11502
+ * by `maxWaitMs` (20 minutes by default, one full lockout and change), and
11503
+ * throws a refusal at once. See `waitForKeepToken`.
11504
+ */
11505
+ waitForToken(options) {
11506
+ return (0, wait_for_token_1.waitForKeepToken)(() => this.getToken(), options);
11507
+ }
11508
+ async refresh() {
11509
+ try {
11510
+ this.rateLimit.check();
11511
+ return await this.exchange();
11512
+ } catch (err) {
11513
+ const code = err?.code;
11514
+ if (this.token && Date.now() < this.expiresAt - STALE_MARGIN_MS && (code === "SERVICE_UNAVAILABLE" || code === "RATE_LIMITED")) {
11515
+ return this.token;
11516
+ }
11517
+ throw err;
11518
+ }
11519
+ }
11247
11520
  async exchange() {
11248
- const url = `${this.keepApiUrl}/api/service-tokens`;
11249
- const response = await fetch(url, {
11250
- method: "POST",
11251
- headers: { "Content-Type": "application/json" },
11252
- body: JSON.stringify({
11253
- clientId: this.clientId,
11254
- clientSecret: this.clientSecret
11255
- })
11521
+ const data = await (0, exchange_1.exchangeCredentials)({
11522
+ manager: "ServiceTokenManager",
11523
+ url: `${this.keepApiUrl}/api/service-tokens`,
11524
+ clientId: this.clientId,
11525
+ clientSecret: this.clientSecret,
11526
+ rateLimit: this.rateLimit,
11527
+ onResponse: this.onResponse,
11528
+ exchangeName: "Token exchange"
11256
11529
  });
11257
- this.onResponse?.(response);
11258
- const json = await response.json();
11259
- if (!response.ok || !json.success) {
11260
- const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
11261
- throw new Error(`[ServiceTokenManager] ${msg}`);
11262
- }
11263
- this.token = json.data.token;
11264
- this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
11530
+ this.token = data.token;
11531
+ this.expiresAt = Date.now() + data.expiresIn * 1e3;
11265
11532
  return this.token;
11266
11533
  }
11267
11534
  };
11268
11535
  exports2.ServiceTokenManager = ServiceTokenManager;
11536
+ var STALE_MARGIN_MS = 5e3;
11269
11537
  }
11270
11538
  });
11271
11539
 
@@ -11274,7 +11542,9 @@ var require_agent_token = __commonJS({
11274
11542
  "../../packages/keep/dist/agent-token.js"(exports2) {
11275
11543
  "use strict";
11276
11544
  Object.defineProperty(exports2, "__esModule", { value: true });
11277
- exports2.AgentTokenManager = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = void 0;
11545
+ exports2.AgentTokenManager = exports2.AgentCredentialRevokedError = void 0;
11546
+ var rate_limited_1 = require_rate_limited();
11547
+ var exchange_1 = require_exchange();
11278
11548
  var AgentCredentialRevokedError2 = class extends Error {
11279
11549
  code = "PAIRING_REVOKED";
11280
11550
  constructor(message) {
@@ -11283,24 +11553,12 @@ var require_agent_token = __commonJS({
11283
11553
  }
11284
11554
  };
11285
11555
  exports2.AgentCredentialRevokedError = AgentCredentialRevokedError2;
11286
- var KeepUnavailableError = class extends Error {
11287
- code = "SERVICE_UNAVAILABLE";
11288
- /** Host the exchange was sent to, so a log line names the service that is down. */
11289
- host;
11290
- /** The status whatever answered sent, or null when nothing answered at all. */
11291
- statusCode;
11292
- constructor(message, detail) {
11293
- super(message, detail.cause !== void 0 ? { cause: detail.cause } : void 0);
11294
- this.name = "KeepUnavailableError";
11295
- this.host = detail.host;
11296
- this.statusCode = detail.statusCode;
11297
- }
11298
- };
11299
- exports2.KeepUnavailableError = KeepUnavailableError;
11300
11556
  var AgentTokenManager = class {
11301
11557
  token = null;
11302
11558
  expiresAt = 0;
11303
11559
  pendingExchange = null;
11560
+ /** Closed by a 429 until Keep's retry-after: no exchange is sent before it. */
11561
+ rateLimit = new rate_limited_1.RateLimitGate();
11304
11562
  keepApiUrl;
11305
11563
  clientId;
11306
11564
  clientSecret;
@@ -11322,6 +11580,7 @@ var require_agent_token = __commonJS({
11322
11580
  if (this.pendingExchange) {
11323
11581
  return this.pendingExchange;
11324
11582
  }
11583
+ this.rateLimit.check();
11325
11584
  this.pendingExchange = this.exchange();
11326
11585
  try {
11327
11586
  return await this.pendingExchange;
@@ -11330,66 +11589,22 @@ var require_agent_token = __commonJS({
11330
11589
  }
11331
11590
  }
11332
11591
  async exchange() {
11333
- const url = `${this.keepApiUrl}/api/agent-tokens`;
11334
- const host = hostOf(url);
11335
- let response;
11336
- try {
11337
- response = await fetch(url, {
11338
- method: "POST",
11339
- headers: { "Content-Type": "application/json" },
11340
- body: JSON.stringify({
11341
- clientId: this.clientId,
11342
- clientSecret: this.clientSecret
11343
- })
11344
- });
11345
- } catch (err) {
11346
- throw new KeepUnavailableError(`[AgentTokenManager] could not reach Keep at ${host}: ${describeTransportError(err)}`, { host, statusCode: null, cause: err });
11347
- }
11348
- this.onResponse?.(response);
11349
- const text = await response.text().catch(() => "");
11350
- let json;
11351
- try {
11352
- json = JSON.parse(text);
11353
- } catch {
11354
- json = null;
11355
- }
11356
- if (response.status >= 500) {
11357
- const reason = json?.error?.message ?? `Agent token exchange failed with status ${response.status}`;
11358
- throw new KeepUnavailableError(`[AgentTokenManager] Keep at ${host} is unavailable (${response.status}): ${reason}`, { host, statusCode: response.status });
11359
- }
11360
- if (json === null) {
11361
- throw new Error(`[AgentTokenManager] Keep at ${host} answered ${response.status} with a body that is not JSON`);
11362
- }
11363
- if (!response.ok || !json.success) {
11364
- const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
11365
- if (response.status === 401) {
11366
- throw new AgentCredentialRevokedError2(`[AgentTokenManager] Keep refused this agent's credentials (401): the pairing was revoked or the secret rotated. Re-pair to continue.`);
11367
- }
11368
- throw new Error(`[AgentTokenManager] ${msg}`);
11369
- }
11370
- this.token = json.data.token;
11371
- this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
11592
+ const data = await (0, exchange_1.exchangeCredentials)({
11593
+ manager: "AgentTokenManager",
11594
+ url: `${this.keepApiUrl}/api/agent-tokens`,
11595
+ clientId: this.clientId,
11596
+ clientSecret: this.clientSecret,
11597
+ rateLimit: this.rateLimit,
11598
+ onResponse: this.onResponse,
11599
+ exchangeName: "Agent token exchange",
11600
+ refused: () => new AgentCredentialRevokedError2(`[AgentTokenManager] Keep refused this agent's credentials (401): the pairing was revoked or the secret rotated. Re-pair to continue.`)
11601
+ });
11602
+ this.token = data.token;
11603
+ this.expiresAt = Date.now() + data.expiresIn * 1e3;
11372
11604
  return this.token;
11373
11605
  }
11374
11606
  };
11375
11607
  exports2.AgentTokenManager = AgentTokenManager;
11376
- function hostOf(url) {
11377
- try {
11378
- return new URL(url).host;
11379
- } catch {
11380
- return url;
11381
- }
11382
- }
11383
- function describeTransportError(err) {
11384
- if (err instanceof Error) {
11385
- const cause = err.cause;
11386
- if (cause instanceof Error && cause.message && cause.message !== err.message) {
11387
- return `${err.message}: ${cause.message}`;
11388
- }
11389
- return err.message;
11390
- }
11391
- return String(err);
11392
- }
11393
11608
  }
11394
11609
  });
11395
11610
 
@@ -11398,7 +11613,7 @@ var require_dist2 = __commonJS({
11398
11613
  "../../packages/keep/dist/index.js"(exports2) {
11399
11614
  "use strict";
11400
11615
  Object.defineProperty(exports2, "__esModule", { value: true });
11401
- exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11616
+ exports2.waitForKeepToken = exports2.KeepRateLimitedError = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11402
11617
  var client_1 = require_client();
11403
11618
  Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
11404
11619
  return client_1.KeepClient;
@@ -11414,8 +11629,17 @@ var require_dist2 = __commonJS({
11414
11629
  Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
11415
11630
  return agent_token_1.AgentCredentialRevokedError;
11416
11631
  } });
11632
+ var exchange_1 = require_exchange();
11417
11633
  Object.defineProperty(exports2, "KeepUnavailableError", { enumerable: true, get: function() {
11418
- return agent_token_1.KeepUnavailableError;
11634
+ return exchange_1.KeepUnavailableError;
11635
+ } });
11636
+ var rate_limited_1 = require_rate_limited();
11637
+ Object.defineProperty(exports2, "KeepRateLimitedError", { enumerable: true, get: function() {
11638
+ return rate_limited_1.KeepRateLimitedError;
11639
+ } });
11640
+ var wait_for_token_1 = require_wait_for_token();
11641
+ Object.defineProperty(exports2, "waitForKeepToken", { enumerable: true, get: function() {
11642
+ return wait_for_token_1.waitForKeepToken;
11419
11643
  } });
11420
11644
  }
11421
11645
  });
@@ -12282,6 +12506,10 @@ var require_checks = __commonJS({
12282
12506
  return skip("pairing-live", "no readable pairing; see pairing-present");
12283
12507
  }
12284
12508
  const minted = await probes.mintAgentToken(stored.value);
12509
+ if (!minted.ok && minted.rateLimited) {
12510
+ const at = formatInstant(minted.rateLimited.retryAt);
12511
+ return warn("pairing-live", `Keep is rate limiting this pairing's token exchange (429), so the pairing could not be checked; it asked for no retry before ${at}`, subject.phase === "run" ? `nothing to do; \`run\` waits until ${at}, asks once, and starts claiming when Keep issues a token` : `wait until ${at}, then run doctor again`);
12512
+ }
12285
12513
  if (!minted.ok && minted.unavailable) {
12286
12514
  return warn("pairing-live", `Keep did not answer, so the pairing could not be checked: ${oneLine(minted.error).replace(/^\[AgentTokenManager\]\s*/, "")}`, subject.phase === "run" ? "nothing to do if this is a deployment window; `run` waits for Keep and starts claiming when it answers" : `check ${stored.value.keepApiUrl} is reachable, then run doctor again`);
12287
12515
  }
@@ -12339,6 +12567,9 @@ var require_checks = __commonJS({
12339
12567
  const pairing = stored.value;
12340
12568
  return withConfig("jobs-reachable", probes, async (config) => {
12341
12569
  const result = await probes.reachJobs(config, pairing);
12570
+ if (!result.ok && result.rateLimited) {
12571
+ return warn("jobs-reachable", `${config.auth.jobsApiUrl} was not asked: Keep is rate limiting the token exchange until ${formatInstant(result.rateLimited.retryAt)}`, "see pairing-live; the run loop asks once Keep issues a token");
12572
+ }
12342
12573
  if (!result.ok) {
12343
12574
  return warn("jobs-reachable", `${config.auth.jobsApiUrl} did not answer: ${oneLine(result.error)}`, "nothing to do if this is a deployment window; the run loop will keep trying");
12344
12575
  }
@@ -12620,6 +12851,9 @@ var require_checks = __commonJS({
12620
12851
  function osTmpDirOf(probes) {
12621
12852
  return probes.osTmpDir();
12622
12853
  }
12854
+ function formatInstant(epochMs) {
12855
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
12856
+ }
12623
12857
  function oneLine(message) {
12624
12858
  return message.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ");
12625
12859
  }
@@ -20320,6 +20554,20 @@ var require_subscribe = __commonJS({
20320
20554
  return true;
20321
20555
  return err instanceof TypeError && err.message === "fetch failed";
20322
20556
  }
20557
+ function isRateLimited(err) {
20558
+ return err?.code === "RATE_LIMITED";
20559
+ }
20560
+ function rateLimitWaitMs(err) {
20561
+ const e = err ?? {};
20562
+ if (typeof e.retryAfterMs === "number" && Number.isFinite(e.retryAfterMs))
20563
+ return Math.max(0, e.retryAfterMs);
20564
+ if (typeof e.retryAt === "number" && Number.isFinite(e.retryAt))
20565
+ return Math.max(0, e.retryAt - Date.now());
20566
+ return 0;
20567
+ }
20568
+ function formatInstant(epochMs) {
20569
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
20570
+ }
20323
20571
  function describeOutage(err) {
20324
20572
  const e = err ?? {};
20325
20573
  if (typeof e.host === "string" && typeof e.statusCode === "number") {
@@ -20386,11 +20634,33 @@ var require_subscribe = __commonJS({
20386
20634
  let outcome = newOutcome();
20387
20635
  let wakeStartup = null;
20388
20636
  let unavailableSince = null;
20637
+ let rateLimited = null;
20389
20638
  function noteReachable() {
20390
20639
  if (unavailableSince !== null) {
20391
20640
  logger.info(`Sift is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
20392
20641
  unavailableSince = null;
20393
20642
  }
20643
+ if (rateLimited !== null) {
20644
+ logger.info(`${rateLimited.host} stopped rate limiting the Sifter after ${formatDuration(Date.now() - rateLimited.since)}`);
20645
+ rateLimited = null;
20646
+ }
20647
+ }
20648
+ function noteRateLimited(err) {
20649
+ const waitMs = Math.max(pollIntervalMs, rateLimitWaitMs(err));
20650
+ const e = err ?? {};
20651
+ const host = typeof e.host === "string" ? e.host : "the service";
20652
+ if (rateLimited === null) {
20653
+ rateLimited = { since: Date.now(), host };
20654
+ const reason = typeof e.reason === "string" ? `: ${e.reason}` : "";
20655
+ logger.warn(`${host} is rate limiting the Sifter (429${reason}). It will wait and try again at ${formatInstant(Date.now() + waitMs)} (in ${formatDuration(waitMs)}).`);
20656
+ } else {
20657
+ logger.debug?.("still rate limited", {
20658
+ host,
20659
+ rateLimitedFor: formatDuration(Date.now() - rateLimited.since),
20660
+ nextAttemptIn: formatDuration(waitMs)
20661
+ });
20662
+ }
20663
+ return waitMs;
20394
20664
  }
20395
20665
  function noteUnavailable(err) {
20396
20666
  if (unavailableSince === null) {
@@ -20420,16 +20690,22 @@ var require_subscribe = __commonJS({
20420
20690
  }
20421
20691
  async function untilAvailable(call, mySession) {
20422
20692
  for (; ; ) {
20693
+ let pause = pollIntervalMs;
20423
20694
  try {
20424
20695
  const value = await call();
20425
20696
  noteReachable();
20426
20697
  return value;
20427
20698
  } catch (err) {
20428
- if (isFatal(err) || !isUnavailable(err))
20699
+ if (isFatal(err))
20700
+ throw err;
20701
+ if (isRateLimited(err))
20702
+ pause = noteRateLimited(err);
20703
+ else if (isUnavailable(err))
20704
+ noteUnavailable(err);
20705
+ else
20429
20706
  throw err;
20430
- noteUnavailable(err);
20431
20707
  }
20432
- await startupPause(pollIntervalMs);
20708
+ await startupPause(pause);
20433
20709
  if (stopped || mySession !== session)
20434
20710
  return STOPPED;
20435
20711
  }
@@ -20515,6 +20791,7 @@ var require_subscribe = __commonJS({
20515
20791
  async function poll(mySession) {
20516
20792
  if (stopped || mySession !== session)
20517
20793
  return;
20794
+ let nextPollMs = pollIntervalMs;
20518
20795
  if (inflight >= maxConcurrency) {
20519
20796
  schedulePoll(pollIntervalMs);
20520
20797
  return;
@@ -20557,14 +20834,16 @@ var require_subscribe = __commonJS({
20557
20834
  void fail(err);
20558
20835
  return;
20559
20836
  }
20560
- if (isUnavailable(err)) {
20837
+ if (isRateLimited(err)) {
20838
+ nextPollMs = noteRateLimited(err);
20839
+ } else if (isUnavailable(err)) {
20561
20840
  noteUnavailable(err);
20562
20841
  } else {
20563
20842
  logger.error("poll failed", { error: err instanceof Error ? err.message : String(err) });
20564
20843
  }
20565
20844
  }
20566
20845
  if (!stopped && mySession === session)
20567
- schedulePoll(pollIntervalMs);
20846
+ schedulePoll(nextPollMs);
20568
20847
  }
20569
20848
  async function runOnce(available) {
20570
20849
  let dispatch;
@@ -21035,6 +21314,21 @@ var require_probes = __commonJS({
21035
21314
  packageName: options.brand.product.packageName
21036
21315
  });
21037
21316
  };
21317
+ const managers = /* @__PURE__ */ new Map();
21318
+ const tokensFor = (pairing) => {
21319
+ const key = JSON.stringify([pairing.keepApiUrl, pairing.credentials.clientId, pairing.credentials.clientSecret]);
21320
+ let tokens = managers.get(key);
21321
+ if (tokens === void 0) {
21322
+ tokens = new keep_1.AgentTokenManager({
21323
+ keepApiUrl: pairing.keepApiUrl,
21324
+ clientId: pairing.credentials.clientId,
21325
+ clientSecret: pairing.credentials.clientSecret,
21326
+ onResponse: enforceFloor
21327
+ });
21328
+ managers.set(key, tokens);
21329
+ }
21330
+ return tokens;
21331
+ };
21038
21332
  let pairingRead;
21039
21333
  let configRead;
21040
21334
  let scaffoldedRead;
@@ -21163,12 +21457,7 @@ var require_probes = __commonJS({
21163
21457
  return scaffoldedRead;
21164
21458
  },
21165
21459
  async mintAgentToken(pairing) {
21166
- const tokens = new keep_1.AgentTokenManager({
21167
- keepApiUrl: pairing.keepApiUrl,
21168
- clientId: pairing.credentials.clientId,
21169
- clientSecret: pairing.credentials.clientSecret,
21170
- onResponse: enforceFloor
21171
- });
21460
+ const tokens = tokensFor(pairing);
21172
21461
  try {
21173
21462
  const token = await tokens.getToken();
21174
21463
  if (token === null) {
@@ -21178,16 +21467,14 @@ var require_probes = __commonJS({
21178
21467
  } catch (err) {
21179
21468
  if (isRevoked(err) || isUpgradeRequired(err))
21180
21469
  throw err;
21470
+ if (isKeepRateLimited(err)) {
21471
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21472
+ }
21181
21473
  return { ok: false, error: describe(err), ...isKeepUnavailable(err) ? { unavailable: true } : {} };
21182
21474
  }
21183
21475
  },
21184
21476
  async reachJobs(config, pairing) {
21185
- const tokens = new keep_1.AgentTokenManager({
21186
- keepApiUrl: pairing.keepApiUrl,
21187
- clientId: pairing.credentials.clientId,
21188
- clientSecret: pairing.credentials.clientSecret,
21189
- onResponse: enforceFloor
21190
- });
21477
+ const tokens = tokensFor(pairing);
21191
21478
  const client = new jobs_1.JobsClient({
21192
21479
  baseUrl: config.auth.jobsApiUrl,
21193
21480
  ...config.auth.apiKey ? { apiKey: config.auth.apiKey } : {},
@@ -21204,6 +21491,9 @@ var require_probes = __commonJS({
21204
21491
  } catch (err) {
21205
21492
  if (isRevoked(err) || isUpgradeRequired(err))
21206
21493
  throw err;
21494
+ if (isKeepRateLimited(err)) {
21495
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21496
+ }
21207
21497
  return { ok: false, error: describe(err) };
21208
21498
  }
21209
21499
  },
@@ -21299,6 +21589,9 @@ var require_probes = __commonJS({
21299
21589
  function isKeepUnavailable(err) {
21300
21590
  return err instanceof Error && err.name === "KeepUnavailableError";
21301
21591
  }
21592
+ function isKeepRateLimited(err) {
21593
+ return err instanceof Error && err.name === "KeepRateLimitedError";
21594
+ }
21302
21595
  function isUpgradeRequired(err) {
21303
21596
  return err instanceof Error && err.name === "UpgradeRequiredError";
21304
21597
  }
@@ -25442,6 +25735,12 @@ var require_shuttle = __commonJS({
25442
25735
  function isKeepUnavailable(err) {
25443
25736
  return err instanceof Error && err.name === "KeepUnavailableError";
25444
25737
  }
25738
+ function isKeepRateLimited(err) {
25739
+ return err instanceof Error && err.name === "KeepRateLimitedError";
25740
+ }
25741
+ function formatInstant(epochMs) {
25742
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
25743
+ }
25445
25744
  async function runOutputGates(dispatch, response, executor, signal, ctx) {
25446
25745
  let spec;
25447
25746
  try {
@@ -25897,30 +26196,44 @@ var require_shuttle = __commonJS({
25897
26196
  }
25898
26197
  async function mintWaitingOutKeep(mint, brand, options) {
25899
26198
  const maxDelay = options.retryMaxMs ?? KEEP_RETRY_MAX_MS;
25900
- let delay = options.retryInitialMs ?? KEEP_RETRY_INITIAL_MS;
25901
- let unavailableSince = null;
26199
+ const initialDelay = options.retryInitialMs ?? KEEP_RETRY_INITIAL_MS;
26200
+ let delay = initialDelay;
26201
+ let waiting = null;
25902
26202
  for (; ; ) {
26203
+ let pause;
25903
26204
  try {
25904
26205
  const token = await mint();
25905
- if (unavailableSince !== null) {
25906
- options.logger?.info(`Keep is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
26206
+ if (waiting?.kind === "unavailable") {
26207
+ options.logger?.info(`Keep is reachable again after ${formatDuration(Date.now() - waiting.since)}`);
26208
+ } else if (waiting?.kind === "rate-limited") {
26209
+ options.logger?.info(`Keep issued a token; the rate limit lifted after ${formatDuration(Date.now() - waiting.since)}`);
25907
26210
  }
25908
26211
  return token;
25909
26212
  } catch (err) {
25910
- if (!isKeepUnavailable(err))
26213
+ if (isKeepRateLimited(err)) {
26214
+ if (waiting?.kind !== "rate-limited") {
26215
+ waiting = { kind: "rate-limited", since: Date.now() };
26216
+ options.logger?.warn(`Keep is rate limiting this ${brand.product.title}'s token exchange (${err.host ?? "Keep"} answered 429: ${err.reason ?? err.message}). The credentials are not in question; the ${brand.product.title} will wait and ask again at ${formatInstant(err.retryAt)} (in ${formatDuration(err.retryAfterMs)}), and start when Keep issues a token.`);
26217
+ }
26218
+ pause = err.retryAfterMs;
26219
+ delay = initialDelay;
26220
+ } else if (isKeepUnavailable(err)) {
26221
+ if (waiting?.kind !== "unavailable") {
26222
+ waiting = { kind: "unavailable", since: Date.now() };
26223
+ const e = err;
26224
+ const what = typeof e.statusCode === "number" ? `${e.host ?? "Keep"} answered ${e.statusCode}` : `${e.host ?? "Keep"} did not answer`;
26225
+ options.logger?.warn(`Keep is unavailable (${what}). This is typical during a deployment window; the ${brand.product.title} will wait for it and start when it answers.`);
26226
+ }
26227
+ pause = delay;
26228
+ delay = Math.min(delay * 2, maxDelay);
26229
+ } else {
25911
26230
  throw err;
25912
- if (unavailableSince === null) {
25913
- unavailableSince = Date.now();
25914
- const e = err;
25915
- const what = typeof e.statusCode === "number" ? `${e.host ?? "Keep"} answered ${e.statusCode}` : `${e.host ?? "Keep"} did not answer`;
25916
- options.logger?.warn(`Keep is unavailable (${what}). This is typical during a deployment window; the ${brand.product.title} will wait for it and start when it answers.`);
25917
26231
  }
25918
26232
  }
25919
- await abortableSleep(delay, options.signal);
26233
+ await abortableSleep(pause, options.signal);
25920
26234
  if (options.signal?.aborted) {
25921
26235
  throw options.signal.reason instanceof Error ? options.signal.reason : new Error("stopped while waiting for Keep");
25922
26236
  }
25923
- delay = Math.min(delay * 2, maxDelay);
25924
26237
  }
25925
26238
  }
25926
26239
  }
@@ -26548,7 +26861,7 @@ var import_path = require("path");
26548
26861
  var import_promises = require("fs/promises");
26549
26862
  var import_yaml = __toESM(require_dist3());
26550
26863
  var import_shuttle = __toESM(require_dist5());
26551
- var buildVersion = true ? "0.30.9" : pkg.version;
26864
+ var buildVersion = true ? "0.30.11" : pkg.version;
26552
26865
  var sifterBrand = {
26553
26866
  product: {
26554
26867
  id: "sifter",