@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 +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/dist/index.mjs
CHANGED
|
@@ -58,7 +58,7 @@ const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
|
|
|
58
58
|
const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
|
|
59
59
|
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
60
60
|
const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
|
|
61
|
-
const SUPPORTED_SOLANA_NETWORKS = ['solana_devnet', 'solana_mainnet'
|
|
61
|
+
const SUPPORTED_SOLANA_NETWORKS = ['solana_devnet', 'solana_mainnet'];
|
|
62
62
|
const SUPPORTED_SOLANA_NETWORK_SET = new Set(SUPPORTED_SOLANA_NETWORKS);
|
|
63
63
|
const ALLOWED_SERVER_TX_PROGRAMS = new Set([
|
|
64
64
|
BOUNDED_PROGRAM_MAINNET,
|
|
@@ -1080,6 +1080,11 @@ async function createWalletClient(opts) {
|
|
|
1080
1080
|
const DEFAULT_WEBHOOK_KEYS_URL = "https://realtime.bounded.sh/.well-known/bounded-webhook-keys.json";
|
|
1081
1081
|
const DEFAULT_MAX_SKEW_SECONDS = 300;
|
|
1082
1082
|
const DEFAULT_CACHE_TTL_MS = 300000;
|
|
1083
|
+
// Negative-cache TTL for unknown key ids (audit U-053): bounds how often a
|
|
1084
|
+
// forged X-Bounded-Key-Id can force a refetch of the hosted keys endpoint. A
|
|
1085
|
+
// genuinely rotated key waits at most one TTL before a refresh is retried.
|
|
1086
|
+
const DEFAULT_UNKNOWN_KEY_TTL_MS = 60000;
|
|
1087
|
+
const UNKNOWN_KEY_CACHE_MAX = 256;
|
|
1083
1088
|
class WebhookVerificationError extends Error {
|
|
1084
1089
|
constructor(message) {
|
|
1085
1090
|
super(message);
|
|
@@ -1122,9 +1127,32 @@ function utf8Bytes(text) {
|
|
|
1122
1127
|
return bytes;
|
|
1123
1128
|
}
|
|
1124
1129
|
const keyCache = new Map();
|
|
1125
|
-
|
|
1130
|
+
// ---------------------------------------------------------------------------
|
|
1131
|
+
// Negative cache for unknown key ids (audit U-053)
|
|
1132
|
+
//
|
|
1133
|
+
// Maps `${keysUrl}\n${keyId}` -> the epoch ms until which a forced refresh for
|
|
1134
|
+
// that key id is suppressed. Without it, every request carrying an unknown
|
|
1135
|
+
// X-Bounded-Key-Id would evict the key cache and force one fetch of the hosted
|
|
1136
|
+
// keys endpoint — N forged calls = N upstream fetches (refetch DoS). With it,
|
|
1137
|
+
// a repeated unknown key id costs at most one refresh per TTL window, while a
|
|
1138
|
+
// genuinely rotated key waits at most one TTL. Bounded size; when full, the
|
|
1139
|
+
// oldest-inserted entry is evicted (LRU-ish; entries are re-inserted on
|
|
1140
|
+
// update, so Map insertion order approximates recency).
|
|
1141
|
+
// ---------------------------------------------------------------------------
|
|
1142
|
+
const unknownKeyCache = new Map();
|
|
1143
|
+
function recordUnknownKey(cacheKey, notFoundUntil) {
|
|
1144
|
+
unknownKeyCache.delete(cacheKey);
|
|
1145
|
+
unknownKeyCache.set(cacheKey, notFoundUntil);
|
|
1146
|
+
if (unknownKeyCache.size > UNKNOWN_KEY_CACHE_MAX) {
|
|
1147
|
+
const oldest = unknownKeyCache.keys().next().value;
|
|
1148
|
+
if (oldest !== undefined)
|
|
1149
|
+
unknownKeyCache.delete(oldest);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/** Clear the in-memory key caches, including the unknown-key negative cache (mainly for tests). */
|
|
1126
1153
|
function clearWebhookKeyCache() {
|
|
1127
1154
|
keyCache.clear();
|
|
1155
|
+
unknownKeyCache.clear();
|
|
1128
1156
|
}
|
|
1129
1157
|
async function loadKeys(keysUrl, cacheTtlMs, fetchImpl, now) {
|
|
1130
1158
|
const cached = keyCache.get(keysUrl);
|
|
@@ -1202,7 +1230,7 @@ function clearWebhookReplayCache() {
|
|
|
1202
1230
|
* @throws {WebhookVerificationError} if anything fails verification.
|
|
1203
1231
|
*/
|
|
1204
1232
|
async function verifyWebhook(rawBody, headers, opts = {}) {
|
|
1205
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1233
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1206
1234
|
// Resolution order: explicit opts.keysUrl > the configured Bounded network's
|
|
1207
1235
|
// hosted keys (so a staging receiver verifies staging-signed deliveries
|
|
1208
1236
|
// without passing keysUrl) > the production default (fail-closed when the
|
|
@@ -1210,8 +1238,9 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
|
|
|
1210
1238
|
const keysUrl = (_b = (_a = opts.keysUrl) !== null && _a !== void 0 ? _a : getWebhookKeysUrl()) !== null && _b !== void 0 ? _b : DEFAULT_WEBHOOK_KEYS_URL;
|
|
1211
1239
|
const maxSkew = (_c = opts.maxSkewSeconds) !== null && _c !== void 0 ? _c : DEFAULT_MAX_SKEW_SECONDS;
|
|
1212
1240
|
const cacheTtlMs = (_d = opts.cacheTtlMs) !== null && _d !== void 0 ? _d : DEFAULT_CACHE_TTL_MS;
|
|
1213
|
-
const
|
|
1214
|
-
const
|
|
1241
|
+
const unknownKeyTtlMs = (_e = opts.unknownKeyTtlMs) !== null && _e !== void 0 ? _e : DEFAULT_UNKNOWN_KEY_TTL_MS;
|
|
1242
|
+
const fetchImpl = (_f = opts.fetchImpl) !== null && _f !== void 0 ? _f : globalThis.fetch;
|
|
1243
|
+
const now = (_g = opts.now) !== null && _g !== void 0 ? _g : Date.now;
|
|
1215
1244
|
if (typeof fetchImpl !== "function") {
|
|
1216
1245
|
throw new WebhookVerificationError("No fetch implementation available; pass opts.fetchImpl.");
|
|
1217
1246
|
}
|
|
@@ -1232,11 +1261,22 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
|
|
|
1232
1261
|
const keys = await loadKeys(keysUrl, cacheTtlMs, fetchImpl, now);
|
|
1233
1262
|
let key = keys.find((k) => k.id === keyId);
|
|
1234
1263
|
// Cache-miss tolerance: a freshly-rotated key may not be cached yet. Force a
|
|
1235
|
-
// single refresh before giving up
|
|
1264
|
+
// single refresh before giving up — unless this key id recently failed a
|
|
1265
|
+
// refresh, in which case the negative cache (audit U-053) suppresses the
|
|
1266
|
+
// refetch so forged unknown key ids cannot force one upstream fetch per
|
|
1267
|
+
// request.
|
|
1236
1268
|
if (!key) {
|
|
1269
|
+
const unknownCacheKey = `${keysUrl}\n${keyId}`;
|
|
1270
|
+
const notFoundUntil = unknownKeyCache.get(unknownCacheKey);
|
|
1271
|
+
if (notFoundUntil !== undefined && notFoundUntil > now()) {
|
|
1272
|
+
throw new WebhookVerificationError(`No public key found for key id "${keyId}".`);
|
|
1273
|
+
}
|
|
1237
1274
|
keyCache.delete(keysUrl);
|
|
1238
1275
|
const refreshed = await loadKeys(keysUrl, cacheTtlMs, fetchImpl, now);
|
|
1239
1276
|
key = refreshed.find((k) => k.id === keyId);
|
|
1277
|
+
if (!key) {
|
|
1278
|
+
recordUnknownKey(unknownCacheKey, now() + unknownKeyTtlMs);
|
|
1279
|
+
}
|
|
1240
1280
|
}
|
|
1241
1281
|
if (!key) {
|
|
1242
1282
|
throw new WebhookVerificationError(`No public key found for key id "${keyId}".`);
|
|
@@ -1265,7 +1305,7 @@ async function verifyWebhook(rawBody, headers, opts = {}) {
|
|
|
1265
1305
|
try {
|
|
1266
1306
|
payload = JSON.parse(rawBody);
|
|
1267
1307
|
}
|
|
1268
|
-
catch (
|
|
1308
|
+
catch (_h) {
|
|
1269
1309
|
throw new WebhookVerificationError("Webhook body is not valid JSON.");
|
|
1270
1310
|
}
|
|
1271
1311
|
if (!payload ||
|