@bounded-sh/server 0.0.52 → 0.0.53-next.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/README.md +1 -1
- package/dist/index.js +47 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -7
- package/dist/index.mjs.map +1 -1
- package/dist/webhooks.d.ts +8 -1
- package/package.json +2 -2
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
|
|
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'
|
|
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
|
-
|
|
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
|
|
1234
|
-
const
|
|
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 (
|
|
1328
|
+
catch (_h) {
|
|
1289
1329
|
throw new WebhookVerificationError("Webhook body is not valid JSON.");
|
|
1290
1330
|
}
|
|
1291
1331
|
if (!payload ||
|