@bounded-sh/server 0.0.52 → 0.0.53-next.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.
package/README.md CHANGED
@@ -189,7 +189,7 @@ class SolanaKeypairProvider implements AuthProvider {
189
189
  ```
190
190
 
191
191
  `runTransaction` requires an explicit provider `rpcUrl` plus `solTransactionData.network`
192
- set to `solana_devnet`, `solana_mainnet`, or `surfnet`; missing RPCs and unknown/missing
192
+ set to `solana_devnet` or `solana_mainnet`; missing RPCs and unknown/missing
193
193
  networks fail closed.
194
194
 
195
195
  ## Types
package/dist/index.js CHANGED
@@ -78,7 +78,7 @@ const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
78
78
  const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
79
79
  const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
80
80
  const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
81
- const SUPPORTED_SOLANA_NETWORKS = ['solana_devnet', 'solana_mainnet', 'surfnet'];
81
+ const SUPPORTED_SOLANA_NETWORKS = ['solana_devnet', 'solana_mainnet'];
82
82
  const SUPPORTED_SOLANA_NETWORK_SET = new Set(SUPPORTED_SOLANA_NETWORKS);
83
83
  const ALLOWED_SERVER_TX_PROGRAMS = new Set([
84
84
  BOUNDED_PROGRAM_MAINNET,
@@ -1100,6 +1100,11 @@ async function createWalletClient(opts) {
1100
1100
  const DEFAULT_WEBHOOK_KEYS_URL = "https://realtime.bounded.sh/.well-known/bounded-webhook-keys.json";
1101
1101
  const DEFAULT_MAX_SKEW_SECONDS = 300;
1102
1102
  const DEFAULT_CACHE_TTL_MS = 300000;
1103
+ // Negative-cache TTL for unknown key ids (audit U-053): bounds how often a
1104
+ // forged X-Bounded-Key-Id can force a refetch of the hosted keys endpoint. A
1105
+ // genuinely rotated key waits at most one TTL before a refresh is retried.
1106
+ const DEFAULT_UNKNOWN_KEY_TTL_MS = 60000;
1107
+ const UNKNOWN_KEY_CACHE_MAX = 256;
1103
1108
  class WebhookVerificationError extends Error {
1104
1109
  constructor(message) {
1105
1110
  super(message);
@@ -1142,9 +1147,32 @@ function utf8Bytes(text) {
1142
1147
  return bytes;
1143
1148
  }
1144
1149
  const keyCache = new Map();
1145
- /** Clear the in-memory key cache (mainly for tests). */
1150
+ // ---------------------------------------------------------------------------
1151
+ // Negative cache for unknown key ids (audit U-053)
1152
+ //
1153
+ // Maps `${keysUrl}\n${keyId}` -> the epoch ms until which a forced refresh for
1154
+ // that key id is suppressed. Without it, every request carrying an unknown
1155
+ // X-Bounded-Key-Id would evict the key cache and force one fetch of the hosted
1156
+ // keys endpoint — N forged calls = N upstream fetches (refetch DoS). With it,
1157
+ // a repeated unknown key id costs at most one refresh per TTL window, while a
1158
+ // genuinely rotated key waits at most one TTL. Bounded size; when full, the
1159
+ // oldest-inserted entry is evicted (LRU-ish; entries are re-inserted on
1160
+ // update, so Map insertion order approximates recency).
1161
+ // ---------------------------------------------------------------------------
1162
+ const unknownKeyCache = new Map();
1163
+ function recordUnknownKey(cacheKey, notFoundUntil) {
1164
+ unknownKeyCache.delete(cacheKey);
1165
+ unknownKeyCache.set(cacheKey, notFoundUntil);
1166
+ if (unknownKeyCache.size > UNKNOWN_KEY_CACHE_MAX) {
1167
+ const oldest = unknownKeyCache.keys().next().value;
1168
+ if (oldest !== undefined)
1169
+ unknownKeyCache.delete(oldest);
1170
+ }
1171
+ }
1172
+ /** Clear the in-memory key caches, including the unknown-key negative cache (mainly for tests). */
1146
1173
  function clearWebhookKeyCache() {
1147
1174
  keyCache.clear();
1175
+ unknownKeyCache.clear();
1148
1176
  }
1149
1177
  async function loadKeys(keysUrl, cacheTtlMs, fetchImpl, now) {
1150
1178
  const cached = keyCache.get(keysUrl);
@@ -1222,7 +1250,7 @@ function clearWebhookReplayCache() {
1222
1250
  * @throws {WebhookVerificationError} if anything fails verification.
1223
1251
  */
1224
1252
  async function verifyWebhook(rawBody, headers, opts = {}) {
1225
- var _a, _b, _c, _d, _e, _f;
1253
+ var _a, _b, _c, _d, _e, _f, _g;
1226
1254
  // Resolution order: explicit opts.keysUrl > the configured Bounded network's
1227
1255
  // hosted keys (so a staging receiver verifies staging-signed deliveries
1228
1256
  // without passing keysUrl) > the production default (fail-closed when the
@@ -1230,8 +1258,9 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
1230
1258
  const keysUrl = (_b = (_a = opts.keysUrl) !== null && _a !== void 0 ? _a : core.getWebhookKeysUrl()) !== null && _b !== void 0 ? _b : DEFAULT_WEBHOOK_KEYS_URL;
1231
1259
  const maxSkew = (_c = opts.maxSkewSeconds) !== null && _c !== void 0 ? _c : DEFAULT_MAX_SKEW_SECONDS;
1232
1260
  const cacheTtlMs = (_d = opts.cacheTtlMs) !== null && _d !== void 0 ? _d : DEFAULT_CACHE_TTL_MS;
1233
- const fetchImpl = (_e = opts.fetchImpl) !== null && _e !== void 0 ? _e : globalThis.fetch;
1234
- const now = (_f = opts.now) !== null && _f !== void 0 ? _f : Date.now;
1261
+ const unknownKeyTtlMs = (_e = opts.unknownKeyTtlMs) !== null && _e !== void 0 ? _e : DEFAULT_UNKNOWN_KEY_TTL_MS;
1262
+ const fetchImpl = (_f = opts.fetchImpl) !== null && _f !== void 0 ? _f : globalThis.fetch;
1263
+ const now = (_g = opts.now) !== null && _g !== void 0 ? _g : Date.now;
1235
1264
  if (typeof fetchImpl !== "function") {
1236
1265
  throw new WebhookVerificationError("No fetch implementation available; pass opts.fetchImpl.");
1237
1266
  }
@@ -1252,11 +1281,22 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
1252
1281
  const keys = await loadKeys(keysUrl, cacheTtlMs, fetchImpl, now);
1253
1282
  let key = keys.find((k) => k.id === keyId);
1254
1283
  // Cache-miss tolerance: a freshly-rotated key may not be cached yet. Force a
1255
- // single refresh before giving up.
1284
+ // single refresh before giving up — unless this key id recently failed a
1285
+ // refresh, in which case the negative cache (audit U-053) suppresses the
1286
+ // refetch so forged unknown key ids cannot force one upstream fetch per
1287
+ // request.
1256
1288
  if (!key) {
1289
+ const unknownCacheKey = `${keysUrl}\n${keyId}`;
1290
+ const notFoundUntil = unknownKeyCache.get(unknownCacheKey);
1291
+ if (notFoundUntil !== undefined && notFoundUntil > now()) {
1292
+ throw new WebhookVerificationError(`No public key found for key id "${keyId}".`);
1293
+ }
1257
1294
  keyCache.delete(keysUrl);
1258
1295
  const refreshed = await loadKeys(keysUrl, cacheTtlMs, fetchImpl, now);
1259
1296
  key = refreshed.find((k) => k.id === keyId);
1297
+ if (!key) {
1298
+ recordUnknownKey(unknownCacheKey, now() + unknownKeyTtlMs);
1299
+ }
1260
1300
  }
1261
1301
  if (!key) {
1262
1302
  throw new WebhookVerificationError(`No public key found for key id "${keyId}".`);
@@ -1285,7 +1325,7 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
1285
1325
  try {
1286
1326
  payload = JSON.parse(rawBody);
1287
1327
  }
1288
- catch (_g) {
1328
+ catch (_h) {
1289
1329
  throw new WebhookVerificationError("Webhook body is not valid JSON.");
1290
1330
  }
1291
1331
  if (!payload ||