nextjs-secure 0.7.0 → 0.8.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 +228 -1
- package/dist/api.cjs +1707 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +708 -0
- package/dist/api.d.ts +708 -0
- package/dist/api.js +1650 -0
- package/dist/api.js.map +1 -0
- package/dist/index.cjs +1674 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1631 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/dist/index.cjs
CHANGED
|
@@ -6584,9 +6584,1639 @@ function getClientIP5(req) {
|
|
|
6584
6584
|
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || "unknown";
|
|
6585
6585
|
}
|
|
6586
6586
|
|
|
6587
|
+
// src/middleware/api/types.ts
|
|
6588
|
+
var API_PROTECTION_PRESETS = {
|
|
6589
|
+
/** Basic: Only timestamp and versioning */
|
|
6590
|
+
basic: {
|
|
6591
|
+
signing: false,
|
|
6592
|
+
replay: false,
|
|
6593
|
+
timestamp: {
|
|
6594
|
+
maxAge: 600,
|
|
6595
|
+
// 10 minutes
|
|
6596
|
+
required: false
|
|
6597
|
+
},
|
|
6598
|
+
versioning: false,
|
|
6599
|
+
idempotency: false
|
|
6600
|
+
},
|
|
6601
|
+
/** Standard: Timestamp, replay prevention, versioning */
|
|
6602
|
+
standard: {
|
|
6603
|
+
signing: false,
|
|
6604
|
+
replay: {
|
|
6605
|
+
ttl: 3e5,
|
|
6606
|
+
// 5 minutes
|
|
6607
|
+
required: true
|
|
6608
|
+
},
|
|
6609
|
+
timestamp: {
|
|
6610
|
+
maxAge: 300,
|
|
6611
|
+
// 5 minutes
|
|
6612
|
+
required: true
|
|
6613
|
+
},
|
|
6614
|
+
versioning: false,
|
|
6615
|
+
idempotency: {
|
|
6616
|
+
required: false
|
|
6617
|
+
}
|
|
6618
|
+
},
|
|
6619
|
+
/** Strict: All protections enabled */
|
|
6620
|
+
strict: {
|
|
6621
|
+
signing: {
|
|
6622
|
+
secret: "",
|
|
6623
|
+
// Must be provided
|
|
6624
|
+
algorithm: "sha256",
|
|
6625
|
+
timestampTolerance: 300
|
|
6626
|
+
},
|
|
6627
|
+
replay: {
|
|
6628
|
+
ttl: 3e5,
|
|
6629
|
+
required: true,
|
|
6630
|
+
minLength: 32
|
|
6631
|
+
},
|
|
6632
|
+
timestamp: {
|
|
6633
|
+
maxAge: 300,
|
|
6634
|
+
required: true,
|
|
6635
|
+
allowFuture: false
|
|
6636
|
+
},
|
|
6637
|
+
versioning: false,
|
|
6638
|
+
idempotency: {
|
|
6639
|
+
required: true,
|
|
6640
|
+
methods: ["POST", "PUT", "PATCH", "DELETE"]
|
|
6641
|
+
}
|
|
6642
|
+
},
|
|
6643
|
+
/** Financial: Maximum security for financial APIs */
|
|
6644
|
+
financial: {
|
|
6645
|
+
signing: {
|
|
6646
|
+
secret: "",
|
|
6647
|
+
// Must be provided
|
|
6648
|
+
algorithm: "sha512",
|
|
6649
|
+
timestampTolerance: 60,
|
|
6650
|
+
// 1 minute
|
|
6651
|
+
components: {
|
|
6652
|
+
method: true,
|
|
6653
|
+
path: true,
|
|
6654
|
+
query: true,
|
|
6655
|
+
body: true,
|
|
6656
|
+
timestamp: true,
|
|
6657
|
+
nonce: true
|
|
6658
|
+
}
|
|
6659
|
+
},
|
|
6660
|
+
replay: {
|
|
6661
|
+
ttl: 864e5,
|
|
6662
|
+
// 24 hours
|
|
6663
|
+
required: true,
|
|
6664
|
+
minLength: 64
|
|
6665
|
+
},
|
|
6666
|
+
timestamp: {
|
|
6667
|
+
maxAge: 60,
|
|
6668
|
+
// 1 minute
|
|
6669
|
+
required: true,
|
|
6670
|
+
allowFuture: false,
|
|
6671
|
+
maxFuture: 10
|
|
6672
|
+
},
|
|
6673
|
+
versioning: false,
|
|
6674
|
+
idempotency: {
|
|
6675
|
+
required: true,
|
|
6676
|
+
ttl: 6048e5,
|
|
6677
|
+
// 7 days
|
|
6678
|
+
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
6679
|
+
hashRequestBody: true
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
};
|
|
6683
|
+
|
|
6684
|
+
// src/middleware/api/signing.ts
|
|
6685
|
+
var DEFAULT_SIGNING_OPTIONS = {
|
|
6686
|
+
algorithm: "sha256",
|
|
6687
|
+
encoding: "hex",
|
|
6688
|
+
signatureHeader: "x-signature",
|
|
6689
|
+
timestampHeader: "x-timestamp",
|
|
6690
|
+
nonceHeader: "x-nonce",
|
|
6691
|
+
components: {
|
|
6692
|
+
method: true,
|
|
6693
|
+
path: true,
|
|
6694
|
+
query: true,
|
|
6695
|
+
body: true,
|
|
6696
|
+
headers: [],
|
|
6697
|
+
timestamp: true,
|
|
6698
|
+
nonce: false
|
|
6699
|
+
},
|
|
6700
|
+
timestampTolerance: 300
|
|
6701
|
+
// 5 minutes
|
|
6702
|
+
};
|
|
6703
|
+
async function createHMAC(data, secret, algorithm = "sha256", encoding = "hex") {
|
|
6704
|
+
const encoder3 = new TextEncoder();
|
|
6705
|
+
const keyData = encoder3.encode(secret);
|
|
6706
|
+
const messageData = encoder3.encode(data);
|
|
6707
|
+
const hashName = {
|
|
6708
|
+
sha1: "SHA-1",
|
|
6709
|
+
sha256: "SHA-256",
|
|
6710
|
+
sha384: "SHA-384",
|
|
6711
|
+
sha512: "SHA-512"
|
|
6712
|
+
}[algorithm];
|
|
6713
|
+
const key = await crypto.subtle.importKey(
|
|
6714
|
+
"raw",
|
|
6715
|
+
keyData,
|
|
6716
|
+
{ name: "HMAC", hash: hashName },
|
|
6717
|
+
false,
|
|
6718
|
+
["sign"]
|
|
6719
|
+
);
|
|
6720
|
+
const signature = await crypto.subtle.sign("HMAC", key, messageData);
|
|
6721
|
+
return encodeSignature(new Uint8Array(signature), encoding);
|
|
6722
|
+
}
|
|
6723
|
+
function encodeSignature(bytes, encoding) {
|
|
6724
|
+
switch (encoding) {
|
|
6725
|
+
case "hex":
|
|
6726
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6727
|
+
case "base64":
|
|
6728
|
+
return btoa(String.fromCharCode(...bytes));
|
|
6729
|
+
case "base64url":
|
|
6730
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6731
|
+
default:
|
|
6732
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
function timingSafeEqual(a, b) {
|
|
6736
|
+
if (a.length !== b.length) {
|
|
6737
|
+
return false;
|
|
6738
|
+
}
|
|
6739
|
+
let result = 0;
|
|
6740
|
+
for (let i = 0; i < a.length; i++) {
|
|
6741
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6742
|
+
}
|
|
6743
|
+
return result === 0;
|
|
6744
|
+
}
|
|
6745
|
+
async function buildCanonicalString(req, components, options = {}) {
|
|
6746
|
+
const parts = [];
|
|
6747
|
+
if (components.method) {
|
|
6748
|
+
parts.push(req.method.toUpperCase());
|
|
6749
|
+
}
|
|
6750
|
+
if (components.path) {
|
|
6751
|
+
const url = new URL(req.url);
|
|
6752
|
+
parts.push(url.pathname);
|
|
6753
|
+
}
|
|
6754
|
+
if (components.query) {
|
|
6755
|
+
const url = new URL(req.url);
|
|
6756
|
+
const params = new URLSearchParams(url.search);
|
|
6757
|
+
const sortedParams = Array.from(params.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([k, v]) => `${k}=${v}`).join("&");
|
|
6758
|
+
parts.push(sortedParams);
|
|
6759
|
+
}
|
|
6760
|
+
if (components.body) {
|
|
6761
|
+
try {
|
|
6762
|
+
const cloned = req.clone();
|
|
6763
|
+
const body = await cloned.text();
|
|
6764
|
+
if (body) {
|
|
6765
|
+
parts.push(body);
|
|
6766
|
+
}
|
|
6767
|
+
} catch {
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
if (components.headers && components.headers.length > 0) {
|
|
6771
|
+
const headerParts = components.headers.map((h) => h.toLowerCase()).sort().map((h) => `${h}:${req.headers.get(h) || ""}`);
|
|
6772
|
+
parts.push(headerParts.join("\n"));
|
|
6773
|
+
}
|
|
6774
|
+
if (components.timestamp) {
|
|
6775
|
+
const timestampHeader = options.timestampHeader || "x-timestamp";
|
|
6776
|
+
const timestamp = req.headers.get(timestampHeader) || "";
|
|
6777
|
+
parts.push(timestamp);
|
|
6778
|
+
}
|
|
6779
|
+
if (components.nonce) {
|
|
6780
|
+
const nonceHeader = options.nonceHeader || "x-nonce";
|
|
6781
|
+
const nonce = req.headers.get(nonceHeader) || "";
|
|
6782
|
+
parts.push(nonce);
|
|
6783
|
+
}
|
|
6784
|
+
return parts.join("\n");
|
|
6785
|
+
}
|
|
6786
|
+
async function generateSignature(req, options) {
|
|
6787
|
+
const {
|
|
6788
|
+
secret,
|
|
6789
|
+
algorithm = DEFAULT_SIGNING_OPTIONS.algorithm,
|
|
6790
|
+
encoding = DEFAULT_SIGNING_OPTIONS.encoding,
|
|
6791
|
+
components = DEFAULT_SIGNING_OPTIONS.components,
|
|
6792
|
+
timestampHeader = DEFAULT_SIGNING_OPTIONS.timestampHeader,
|
|
6793
|
+
nonceHeader = DEFAULT_SIGNING_OPTIONS.nonceHeader,
|
|
6794
|
+
canonicalBuilder
|
|
6795
|
+
} = options;
|
|
6796
|
+
const canonical = canonicalBuilder ? await canonicalBuilder(req, components) : await buildCanonicalString(req, components, { timestampHeader, nonceHeader });
|
|
6797
|
+
return createHMAC(canonical, secret, algorithm, encoding);
|
|
6798
|
+
}
|
|
6799
|
+
async function generateSignatureHeaders(method, url, body, options) {
|
|
6800
|
+
const {
|
|
6801
|
+
secret,
|
|
6802
|
+
algorithm = DEFAULT_SIGNING_OPTIONS.algorithm,
|
|
6803
|
+
encoding = DEFAULT_SIGNING_OPTIONS.encoding,
|
|
6804
|
+
components = DEFAULT_SIGNING_OPTIONS.components,
|
|
6805
|
+
signatureHeader = DEFAULT_SIGNING_OPTIONS.signatureHeader,
|
|
6806
|
+
timestampHeader = DEFAULT_SIGNING_OPTIONS.timestampHeader,
|
|
6807
|
+
nonceHeader = DEFAULT_SIGNING_OPTIONS.nonceHeader,
|
|
6808
|
+
includeTimestamp = true,
|
|
6809
|
+
includeNonce = false
|
|
6810
|
+
} = options;
|
|
6811
|
+
const headers = {};
|
|
6812
|
+
const parts = [];
|
|
6813
|
+
if (components.method) {
|
|
6814
|
+
parts.push(method.toUpperCase());
|
|
6815
|
+
}
|
|
6816
|
+
if (components.path) {
|
|
6817
|
+
const parsedUrl = new URL(url);
|
|
6818
|
+
parts.push(parsedUrl.pathname);
|
|
6819
|
+
}
|
|
6820
|
+
if (components.query) {
|
|
6821
|
+
const parsedUrl = new URL(url);
|
|
6822
|
+
const params = new URLSearchParams(parsedUrl.search);
|
|
6823
|
+
const sortedParams = Array.from(params.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([k, v]) => `${k}=${v}`).join("&");
|
|
6824
|
+
parts.push(sortedParams);
|
|
6825
|
+
}
|
|
6826
|
+
if (components.body && body) {
|
|
6827
|
+
parts.push(body);
|
|
6828
|
+
}
|
|
6829
|
+
if (components.timestamp && includeTimestamp) {
|
|
6830
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
6831
|
+
headers[timestampHeader] = timestamp;
|
|
6832
|
+
parts.push(timestamp);
|
|
6833
|
+
}
|
|
6834
|
+
if (components.nonce && includeNonce) {
|
|
6835
|
+
const nonce = generateNonce();
|
|
6836
|
+
headers[nonceHeader] = nonce;
|
|
6837
|
+
parts.push(nonce);
|
|
6838
|
+
}
|
|
6839
|
+
const canonical = parts.join("\n");
|
|
6840
|
+
const signature = await createHMAC(canonical, secret, algorithm, encoding);
|
|
6841
|
+
headers[signatureHeader] = signature;
|
|
6842
|
+
return headers;
|
|
6843
|
+
}
|
|
6844
|
+
function generateNonce(length = 32) {
|
|
6845
|
+
const bytes = new Uint8Array(length);
|
|
6846
|
+
crypto.getRandomValues(bytes);
|
|
6847
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6848
|
+
}
|
|
6849
|
+
async function verifySignature(req, options) {
|
|
6850
|
+
const {
|
|
6851
|
+
secret,
|
|
6852
|
+
algorithm = DEFAULT_SIGNING_OPTIONS.algorithm,
|
|
6853
|
+
encoding = DEFAULT_SIGNING_OPTIONS.encoding,
|
|
6854
|
+
signatureHeader = DEFAULT_SIGNING_OPTIONS.signatureHeader,
|
|
6855
|
+
timestampHeader = DEFAULT_SIGNING_OPTIONS.timestampHeader,
|
|
6856
|
+
nonceHeader = DEFAULT_SIGNING_OPTIONS.nonceHeader,
|
|
6857
|
+
components = DEFAULT_SIGNING_OPTIONS.components,
|
|
6858
|
+
timestampTolerance = DEFAULT_SIGNING_OPTIONS.timestampTolerance,
|
|
6859
|
+
canonicalBuilder
|
|
6860
|
+
} = options;
|
|
6861
|
+
const providedSignature = req.headers.get(signatureHeader);
|
|
6862
|
+
if (!providedSignature) {
|
|
6863
|
+
return {
|
|
6864
|
+
valid: false,
|
|
6865
|
+
reason: "Missing signature header"
|
|
6866
|
+
};
|
|
6867
|
+
}
|
|
6868
|
+
if (components.timestamp) {
|
|
6869
|
+
const timestamp = req.headers.get(timestampHeader);
|
|
6870
|
+
if (!timestamp) {
|
|
6871
|
+
return {
|
|
6872
|
+
valid: false,
|
|
6873
|
+
reason: "Missing timestamp header"
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
const timestampNum = parseInt(timestamp, 10);
|
|
6877
|
+
if (isNaN(timestampNum)) {
|
|
6878
|
+
return {
|
|
6879
|
+
valid: false,
|
|
6880
|
+
reason: "Invalid timestamp format"
|
|
6881
|
+
};
|
|
6882
|
+
}
|
|
6883
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
6884
|
+
const age = Math.abs(now - timestampNum);
|
|
6885
|
+
if (age > timestampTolerance) {
|
|
6886
|
+
return {
|
|
6887
|
+
valid: false,
|
|
6888
|
+
reason: `Timestamp too old or too far in future (age: ${age}s, max: ${timestampTolerance}s)`
|
|
6889
|
+
};
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
const canonical = canonicalBuilder ? await canonicalBuilder(req, components) : await buildCanonicalString(req, components, { timestampHeader, nonceHeader });
|
|
6893
|
+
const computedSignature = await createHMAC(canonical, secret, algorithm, encoding);
|
|
6894
|
+
const valid = timingSafeEqual(providedSignature, computedSignature);
|
|
6895
|
+
return {
|
|
6896
|
+
valid,
|
|
6897
|
+
reason: valid ? void 0 : "Signature mismatch",
|
|
6898
|
+
computed: computedSignature,
|
|
6899
|
+
provided: providedSignature,
|
|
6900
|
+
canonical
|
|
6901
|
+
};
|
|
6902
|
+
}
|
|
6903
|
+
function defaultInvalidResponse(reason) {
|
|
6904
|
+
return new Response(
|
|
6905
|
+
JSON.stringify({
|
|
6906
|
+
error: "Unauthorized",
|
|
6907
|
+
message: reason,
|
|
6908
|
+
code: "INVALID_SIGNATURE"
|
|
6909
|
+
}),
|
|
6910
|
+
{
|
|
6911
|
+
status: 401,
|
|
6912
|
+
headers: { "Content-Type": "application/json" }
|
|
6913
|
+
}
|
|
6914
|
+
);
|
|
6915
|
+
}
|
|
6916
|
+
function withRequestSigning(handler, options) {
|
|
6917
|
+
return async (req, ctx) => {
|
|
6918
|
+
if (options.skip && await options.skip(req)) {
|
|
6919
|
+
return handler(req, ctx);
|
|
6920
|
+
}
|
|
6921
|
+
const result = await verifySignature(req, options);
|
|
6922
|
+
if (!result.valid) {
|
|
6923
|
+
const onInvalid = options.onInvalid || defaultInvalidResponse;
|
|
6924
|
+
return onInvalid(result.reason || "Invalid signature");
|
|
6925
|
+
}
|
|
6926
|
+
return handler(req, ctx);
|
|
6927
|
+
};
|
|
6928
|
+
}
|
|
6929
|
+
|
|
6930
|
+
// src/middleware/api/replay.ts
|
|
6931
|
+
var DEFAULT_REPLAY_OPTIONS = {
|
|
6932
|
+
nonceHeader: "x-nonce",
|
|
6933
|
+
nonceQuery: "",
|
|
6934
|
+
ttl: 3e5,
|
|
6935
|
+
// 5 minutes
|
|
6936
|
+
required: true,
|
|
6937
|
+
minLength: 16,
|
|
6938
|
+
maxLength: 128
|
|
6939
|
+
};
|
|
6940
|
+
var MemoryNonceStore = class {
|
|
6941
|
+
nonces = /* @__PURE__ */ new Map();
|
|
6942
|
+
maxSize;
|
|
6943
|
+
cleanupInterval = null;
|
|
6944
|
+
constructor(options = {}) {
|
|
6945
|
+
const { maxSize = 1e5, autoCleanup = true, cleanupIntervalMs = 6e4 } = options;
|
|
6946
|
+
this.maxSize = maxSize;
|
|
6947
|
+
if (autoCleanup) {
|
|
6948
|
+
this.cleanupInterval = setInterval(() => {
|
|
6949
|
+
this.cleanup();
|
|
6950
|
+
}, cleanupIntervalMs);
|
|
6951
|
+
if (this.cleanupInterval.unref) {
|
|
6952
|
+
this.cleanupInterval.unref();
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
async exists(nonce) {
|
|
6957
|
+
const entry = this.nonces.get(nonce);
|
|
6958
|
+
if (!entry) {
|
|
6959
|
+
return false;
|
|
6960
|
+
}
|
|
6961
|
+
if (Date.now() > entry.expiresAt) {
|
|
6962
|
+
this.nonces.delete(nonce);
|
|
6963
|
+
return false;
|
|
6964
|
+
}
|
|
6965
|
+
return true;
|
|
6966
|
+
}
|
|
6967
|
+
async set(nonce, ttl) {
|
|
6968
|
+
if (this.nonces.size >= this.maxSize) {
|
|
6969
|
+
this.evictOldest();
|
|
6970
|
+
}
|
|
6971
|
+
const now = Date.now();
|
|
6972
|
+
this.nonces.set(nonce, {
|
|
6973
|
+
timestamp: now,
|
|
6974
|
+
expiresAt: now + ttl
|
|
6975
|
+
});
|
|
6976
|
+
}
|
|
6977
|
+
async cleanup() {
|
|
6978
|
+
const now = Date.now();
|
|
6979
|
+
for (const [nonce, entry] of this.nonces.entries()) {
|
|
6980
|
+
if (now > entry.expiresAt) {
|
|
6981
|
+
this.nonces.delete(nonce);
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
getStats() {
|
|
6986
|
+
let oldestTimestamp;
|
|
6987
|
+
for (const entry of this.nonces.values()) {
|
|
6988
|
+
if (!oldestTimestamp || entry.timestamp < oldestTimestamp) {
|
|
6989
|
+
oldestTimestamp = entry.timestamp;
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
return {
|
|
6993
|
+
size: this.nonces.size,
|
|
6994
|
+
oldestTimestamp
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6997
|
+
evictOldest() {
|
|
6998
|
+
const toRemove = Math.ceil(this.maxSize * 0.1);
|
|
6999
|
+
const entries = Array.from(this.nonces.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp).slice(0, toRemove);
|
|
7000
|
+
for (const [nonce] of entries) {
|
|
7001
|
+
this.nonces.delete(nonce);
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
/**
|
|
7005
|
+
* Clear all nonces
|
|
7006
|
+
*/
|
|
7007
|
+
clear() {
|
|
7008
|
+
this.nonces.clear();
|
|
7009
|
+
}
|
|
7010
|
+
/**
|
|
7011
|
+
* Stop auto cleanup
|
|
7012
|
+
*/
|
|
7013
|
+
destroy() {
|
|
7014
|
+
if (this.cleanupInterval) {
|
|
7015
|
+
clearInterval(this.cleanupInterval);
|
|
7016
|
+
this.cleanupInterval = null;
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
};
|
|
7020
|
+
var globalNonceStore = null;
|
|
7021
|
+
function getGlobalNonceStore() {
|
|
7022
|
+
if (!globalNonceStore) {
|
|
7023
|
+
globalNonceStore = new MemoryNonceStore();
|
|
7024
|
+
}
|
|
7025
|
+
return globalNonceStore;
|
|
7026
|
+
}
|
|
7027
|
+
function generateNonce2(length = 32) {
|
|
7028
|
+
const bytes = new Uint8Array(length);
|
|
7029
|
+
crypto.getRandomValues(bytes);
|
|
7030
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7031
|
+
}
|
|
7032
|
+
function isValidNonceFormat(nonce, minLength = 16, maxLength = 128) {
|
|
7033
|
+
if (!nonce || typeof nonce !== "string") {
|
|
7034
|
+
return false;
|
|
7035
|
+
}
|
|
7036
|
+
if (nonce.length < minLength || nonce.length > maxLength) {
|
|
7037
|
+
return false;
|
|
7038
|
+
}
|
|
7039
|
+
return /^[a-zA-Z0-9_-]+$/.test(nonce);
|
|
7040
|
+
}
|
|
7041
|
+
function extractNonce(req, options = {}) {
|
|
7042
|
+
const {
|
|
7043
|
+
nonceHeader = DEFAULT_REPLAY_OPTIONS.nonceHeader,
|
|
7044
|
+
nonceQuery = DEFAULT_REPLAY_OPTIONS.nonceQuery
|
|
7045
|
+
} = options;
|
|
7046
|
+
const headerNonce = req.headers.get(nonceHeader);
|
|
7047
|
+
if (headerNonce) {
|
|
7048
|
+
return headerNonce;
|
|
7049
|
+
}
|
|
7050
|
+
if (nonceQuery) {
|
|
7051
|
+
const url = new URL(req.url);
|
|
7052
|
+
const queryNonce = url.searchParams.get(nonceQuery);
|
|
7053
|
+
if (queryNonce) {
|
|
7054
|
+
return queryNonce;
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
return null;
|
|
7058
|
+
}
|
|
7059
|
+
async function checkReplay(req, options = {}) {
|
|
7060
|
+
const {
|
|
7061
|
+
store = getGlobalNonceStore(),
|
|
7062
|
+
nonceHeader = DEFAULT_REPLAY_OPTIONS.nonceHeader,
|
|
7063
|
+
nonceQuery = DEFAULT_REPLAY_OPTIONS.nonceQuery,
|
|
7064
|
+
ttl = DEFAULT_REPLAY_OPTIONS.ttl,
|
|
7065
|
+
required = DEFAULT_REPLAY_OPTIONS.required,
|
|
7066
|
+
minLength = DEFAULT_REPLAY_OPTIONS.minLength,
|
|
7067
|
+
maxLength = DEFAULT_REPLAY_OPTIONS.maxLength,
|
|
7068
|
+
validate: validate2
|
|
7069
|
+
} = options;
|
|
7070
|
+
const nonce = extractNonce(req, { nonceHeader, nonceQuery });
|
|
7071
|
+
if (!nonce) {
|
|
7072
|
+
if (required) {
|
|
7073
|
+
return {
|
|
7074
|
+
isReplay: false,
|
|
7075
|
+
// Not a replay, but missing
|
|
7076
|
+
nonce: null,
|
|
7077
|
+
reason: "Missing nonce"
|
|
7078
|
+
};
|
|
7079
|
+
}
|
|
7080
|
+
return {
|
|
7081
|
+
isReplay: false
|
|
7082
|
+
};
|
|
7083
|
+
}
|
|
7084
|
+
if (!isValidNonceFormat(nonce, minLength, maxLength)) {
|
|
7085
|
+
return {
|
|
7086
|
+
isReplay: false,
|
|
7087
|
+
nonce,
|
|
7088
|
+
reason: `Invalid nonce format (length must be ${minLength}-${maxLength}, alphanumeric)`
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
7091
|
+
if (validate2) {
|
|
7092
|
+
const isValid = await validate2(nonce);
|
|
7093
|
+
if (!isValid) {
|
|
7094
|
+
return {
|
|
7095
|
+
isReplay: false,
|
|
7096
|
+
nonce,
|
|
7097
|
+
reason: "Nonce failed custom validation"
|
|
7098
|
+
};
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
const exists = await store.exists(nonce);
|
|
7102
|
+
if (exists) {
|
|
7103
|
+
return {
|
|
7104
|
+
isReplay: true,
|
|
7105
|
+
nonce,
|
|
7106
|
+
reason: "Nonce has already been used"
|
|
7107
|
+
};
|
|
7108
|
+
}
|
|
7109
|
+
await store.set(nonce, ttl);
|
|
7110
|
+
return {
|
|
7111
|
+
isReplay: false,
|
|
7112
|
+
nonce
|
|
7113
|
+
};
|
|
7114
|
+
}
|
|
7115
|
+
function defaultReplayResponse(nonce) {
|
|
7116
|
+
return new Response(
|
|
7117
|
+
JSON.stringify({
|
|
7118
|
+
error: "Forbidden",
|
|
7119
|
+
message: "Request replay detected",
|
|
7120
|
+
code: "REPLAY_DETECTED",
|
|
7121
|
+
nonce
|
|
7122
|
+
}),
|
|
7123
|
+
{
|
|
7124
|
+
status: 403,
|
|
7125
|
+
headers: { "Content-Type": "application/json" }
|
|
7126
|
+
}
|
|
7127
|
+
);
|
|
7128
|
+
}
|
|
7129
|
+
function defaultMissingNonceResponse(reason) {
|
|
7130
|
+
return new Response(
|
|
7131
|
+
JSON.stringify({
|
|
7132
|
+
error: "Bad Request",
|
|
7133
|
+
message: reason,
|
|
7134
|
+
code: "INVALID_NONCE"
|
|
7135
|
+
}),
|
|
7136
|
+
{
|
|
7137
|
+
status: 400,
|
|
7138
|
+
headers: { "Content-Type": "application/json" }
|
|
7139
|
+
}
|
|
7140
|
+
);
|
|
7141
|
+
}
|
|
7142
|
+
function withReplayPrevention(handler, options = {}) {
|
|
7143
|
+
return async (req, ctx) => {
|
|
7144
|
+
if (options.skip && await options.skip(req)) {
|
|
7145
|
+
return handler(req, ctx);
|
|
7146
|
+
}
|
|
7147
|
+
const result = await checkReplay(req, options);
|
|
7148
|
+
if (result.isReplay) {
|
|
7149
|
+
const onReplay = options.onReplay || defaultReplayResponse;
|
|
7150
|
+
return onReplay(result.nonce);
|
|
7151
|
+
}
|
|
7152
|
+
if (result.reason && options.required !== false) {
|
|
7153
|
+
return defaultMissingNonceResponse(result.reason);
|
|
7154
|
+
}
|
|
7155
|
+
return handler(req, ctx);
|
|
7156
|
+
};
|
|
7157
|
+
}
|
|
7158
|
+
function addNonceHeader(headers = {}, options = {}) {
|
|
7159
|
+
const { headerName = "x-nonce", length = 32 } = options;
|
|
7160
|
+
return {
|
|
7161
|
+
...headers,
|
|
7162
|
+
[headerName]: generateNonce2(length)
|
|
7163
|
+
};
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// src/middleware/api/timestamp.ts
|
|
7167
|
+
var DEFAULT_TIMESTAMP_OPTIONS = {
|
|
7168
|
+
timestampHeader: "x-timestamp",
|
|
7169
|
+
format: "unix",
|
|
7170
|
+
maxAge: 300,
|
|
7171
|
+
// 5 minutes
|
|
7172
|
+
allowFuture: false,
|
|
7173
|
+
maxFuture: 60,
|
|
7174
|
+
// 1 minute
|
|
7175
|
+
required: true
|
|
7176
|
+
};
|
|
7177
|
+
function parseTimestamp(value, format = "unix") {
|
|
7178
|
+
if (!value || typeof value !== "string") {
|
|
7179
|
+
return null;
|
|
7180
|
+
}
|
|
7181
|
+
try {
|
|
7182
|
+
switch (format) {
|
|
7183
|
+
case "unix": {
|
|
7184
|
+
const num = parseInt(value, 10);
|
|
7185
|
+
if (isNaN(num) || num <= 0) {
|
|
7186
|
+
return null;
|
|
7187
|
+
}
|
|
7188
|
+
return num;
|
|
7189
|
+
}
|
|
7190
|
+
case "unix-ms": {
|
|
7191
|
+
const num = parseInt(value, 10);
|
|
7192
|
+
if (isNaN(num) || num <= 0) {
|
|
7193
|
+
return null;
|
|
7194
|
+
}
|
|
7195
|
+
return Math.floor(num / 1e3);
|
|
7196
|
+
}
|
|
7197
|
+
case "iso8601": {
|
|
7198
|
+
const date = new Date(value);
|
|
7199
|
+
if (isNaN(date.getTime())) {
|
|
7200
|
+
return null;
|
|
7201
|
+
}
|
|
7202
|
+
return Math.floor(date.getTime() / 1e3);
|
|
7203
|
+
}
|
|
7204
|
+
default:
|
|
7205
|
+
return null;
|
|
7206
|
+
}
|
|
7207
|
+
} catch {
|
|
7208
|
+
return null;
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
function formatTimestamp(format = "unix") {
|
|
7212
|
+
const now = Date.now();
|
|
7213
|
+
switch (format) {
|
|
7214
|
+
case "unix":
|
|
7215
|
+
return Math.floor(now / 1e3).toString();
|
|
7216
|
+
case "unix-ms":
|
|
7217
|
+
return now.toString();
|
|
7218
|
+
case "iso8601":
|
|
7219
|
+
return new Date(now).toISOString();
|
|
7220
|
+
default:
|
|
7221
|
+
return Math.floor(now / 1e3).toString();
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
function extractTimestamp(req, options = {}) {
|
|
7225
|
+
const {
|
|
7226
|
+
timestampHeader = DEFAULT_TIMESTAMP_OPTIONS.timestampHeader,
|
|
7227
|
+
timestampQuery
|
|
7228
|
+
} = options;
|
|
7229
|
+
const headerTimestamp = req.headers.get(timestampHeader);
|
|
7230
|
+
if (headerTimestamp) {
|
|
7231
|
+
return headerTimestamp;
|
|
7232
|
+
}
|
|
7233
|
+
if (timestampQuery) {
|
|
7234
|
+
const url = new URL(req.url);
|
|
7235
|
+
const queryTimestamp = url.searchParams.get(timestampQuery);
|
|
7236
|
+
if (queryTimestamp) {
|
|
7237
|
+
return queryTimestamp;
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
return null;
|
|
7241
|
+
}
|
|
7242
|
+
function validateTimestamp(req, options = {}) {
|
|
7243
|
+
const {
|
|
7244
|
+
timestampHeader = DEFAULT_TIMESTAMP_OPTIONS.timestampHeader,
|
|
7245
|
+
timestampQuery,
|
|
7246
|
+
format = DEFAULT_TIMESTAMP_OPTIONS.format,
|
|
7247
|
+
maxAge = DEFAULT_TIMESTAMP_OPTIONS.maxAge,
|
|
7248
|
+
allowFuture = DEFAULT_TIMESTAMP_OPTIONS.allowFuture,
|
|
7249
|
+
maxFuture = DEFAULT_TIMESTAMP_OPTIONS.maxFuture,
|
|
7250
|
+
required = DEFAULT_TIMESTAMP_OPTIONS.required
|
|
7251
|
+
} = options;
|
|
7252
|
+
const timestampStr = extractTimestamp(req, { timestampHeader, timestampQuery });
|
|
7253
|
+
if (!timestampStr) {
|
|
7254
|
+
if (required) {
|
|
7255
|
+
return {
|
|
7256
|
+
valid: false,
|
|
7257
|
+
reason: "Missing timestamp"
|
|
7258
|
+
};
|
|
7259
|
+
}
|
|
7260
|
+
return {
|
|
7261
|
+
valid: true
|
|
7262
|
+
};
|
|
7263
|
+
}
|
|
7264
|
+
const timestamp = parseTimestamp(timestampStr, format);
|
|
7265
|
+
if (timestamp === null) {
|
|
7266
|
+
return {
|
|
7267
|
+
valid: false,
|
|
7268
|
+
reason: `Invalid timestamp format (expected: ${format})`
|
|
7269
|
+
};
|
|
7270
|
+
}
|
|
7271
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7272
|
+
const age = now - timestamp;
|
|
7273
|
+
if (age > maxAge) {
|
|
7274
|
+
return {
|
|
7275
|
+
valid: false,
|
|
7276
|
+
timestamp,
|
|
7277
|
+
age,
|
|
7278
|
+
reason: `Timestamp too old (age: ${age}s, max: ${maxAge}s)`
|
|
7279
|
+
};
|
|
7280
|
+
}
|
|
7281
|
+
if (age < 0) {
|
|
7282
|
+
if (!allowFuture) {
|
|
7283
|
+
return {
|
|
7284
|
+
valid: false,
|
|
7285
|
+
timestamp,
|
|
7286
|
+
age,
|
|
7287
|
+
reason: "Timestamp is in the future"
|
|
7288
|
+
};
|
|
7289
|
+
}
|
|
7290
|
+
const futureAge = Math.abs(age);
|
|
7291
|
+
if (futureAge > maxFuture) {
|
|
7292
|
+
return {
|
|
7293
|
+
valid: false,
|
|
7294
|
+
timestamp,
|
|
7295
|
+
age,
|
|
7296
|
+
reason: `Timestamp too far in future (${futureAge}s, max: ${maxFuture}s)`
|
|
7297
|
+
};
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
return {
|
|
7301
|
+
valid: true,
|
|
7302
|
+
timestamp,
|
|
7303
|
+
age
|
|
7304
|
+
};
|
|
7305
|
+
}
|
|
7306
|
+
function isTimestampValid(timestampStr, options = {}) {
|
|
7307
|
+
const {
|
|
7308
|
+
format = "unix",
|
|
7309
|
+
maxAge = 300,
|
|
7310
|
+
allowFuture = false,
|
|
7311
|
+
maxFuture = 60
|
|
7312
|
+
} = options;
|
|
7313
|
+
const timestamp = parseTimestamp(timestampStr, format);
|
|
7314
|
+
if (timestamp === null) {
|
|
7315
|
+
return false;
|
|
7316
|
+
}
|
|
7317
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7318
|
+
const age = now - timestamp;
|
|
7319
|
+
if (age > maxAge) {
|
|
7320
|
+
return false;
|
|
7321
|
+
}
|
|
7322
|
+
if (age < 0) {
|
|
7323
|
+
if (!allowFuture) {
|
|
7324
|
+
return false;
|
|
7325
|
+
}
|
|
7326
|
+
if (Math.abs(age) > maxFuture) {
|
|
7327
|
+
return false;
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
return true;
|
|
7331
|
+
}
|
|
7332
|
+
function defaultInvalidResponse2(reason) {
|
|
7333
|
+
return new Response(
|
|
7334
|
+
JSON.stringify({
|
|
7335
|
+
error: "Bad Request",
|
|
7336
|
+
message: reason,
|
|
7337
|
+
code: "INVALID_TIMESTAMP"
|
|
7338
|
+
}),
|
|
7339
|
+
{
|
|
7340
|
+
status: 400,
|
|
7341
|
+
headers: { "Content-Type": "application/json" }
|
|
7342
|
+
}
|
|
7343
|
+
);
|
|
7344
|
+
}
|
|
7345
|
+
function withTimestamp(handler, options = {}) {
|
|
7346
|
+
return async (req, ctx) => {
|
|
7347
|
+
if (options.skip && await options.skip(req)) {
|
|
7348
|
+
return handler(req, ctx);
|
|
7349
|
+
}
|
|
7350
|
+
const result = validateTimestamp(req, options);
|
|
7351
|
+
if (!result.valid) {
|
|
7352
|
+
const onInvalid = options.onInvalid || defaultInvalidResponse2;
|
|
7353
|
+
return onInvalid(result.reason || "Invalid timestamp");
|
|
7354
|
+
}
|
|
7355
|
+
return handler(req, ctx);
|
|
7356
|
+
};
|
|
7357
|
+
}
|
|
7358
|
+
function addTimestampHeader(headers = {}, options = {}) {
|
|
7359
|
+
const { headerName = "x-timestamp", format = "unix" } = options;
|
|
7360
|
+
return {
|
|
7361
|
+
...headers,
|
|
7362
|
+
[headerName]: formatTimestamp(format)
|
|
7363
|
+
};
|
|
7364
|
+
}
|
|
7365
|
+
function getRequestAge(req, options = {}) {
|
|
7366
|
+
const timestampStr = extractTimestamp(req, options);
|
|
7367
|
+
if (!timestampStr) {
|
|
7368
|
+
return null;
|
|
7369
|
+
}
|
|
7370
|
+
const timestamp = parseTimestamp(timestampStr, options.format);
|
|
7371
|
+
if (timestamp === null) {
|
|
7372
|
+
return null;
|
|
7373
|
+
}
|
|
7374
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7375
|
+
return now - timestamp;
|
|
7376
|
+
}
|
|
7377
|
+
|
|
7378
|
+
// src/middleware/api/versioning.ts
|
|
7379
|
+
var DEFAULT_VERSIONING_OPTIONS = {
|
|
7380
|
+
source: "header",
|
|
7381
|
+
versionHeader: "x-api-version",
|
|
7382
|
+
versionQuery: "version"};
|
|
7383
|
+
function extractFromHeader(req, headerName) {
|
|
7384
|
+
return req.headers.get(headerName);
|
|
7385
|
+
}
|
|
7386
|
+
function extractFromQuery(req, queryParam) {
|
|
7387
|
+
const url = new URL(req.url);
|
|
7388
|
+
return url.searchParams.get(queryParam);
|
|
7389
|
+
}
|
|
7390
|
+
function extractFromPath(req, pattern) {
|
|
7391
|
+
if (!pattern) {
|
|
7392
|
+
pattern = /\/v(\d+(?:\.\d+)?)\//;
|
|
7393
|
+
}
|
|
7394
|
+
const url = new URL(req.url);
|
|
7395
|
+
const match = url.pathname.match(pattern);
|
|
7396
|
+
if (match && match[1]) {
|
|
7397
|
+
return match[1];
|
|
7398
|
+
}
|
|
7399
|
+
return null;
|
|
7400
|
+
}
|
|
7401
|
+
function extractFromAccept(req, pattern) {
|
|
7402
|
+
const accept = req.headers.get("accept");
|
|
7403
|
+
if (!accept) {
|
|
7404
|
+
return null;
|
|
7405
|
+
}
|
|
7406
|
+
if (!pattern) {
|
|
7407
|
+
pattern = /version=(\d+(?:\.\d+)?)/;
|
|
7408
|
+
}
|
|
7409
|
+
const match = accept.match(pattern);
|
|
7410
|
+
if (match && match[1]) {
|
|
7411
|
+
return match[1];
|
|
7412
|
+
}
|
|
7413
|
+
return null;
|
|
7414
|
+
}
|
|
7415
|
+
function extractVersion(req, options) {
|
|
7416
|
+
const {
|
|
7417
|
+
source = DEFAULT_VERSIONING_OPTIONS.source,
|
|
7418
|
+
versionHeader = DEFAULT_VERSIONING_OPTIONS.versionHeader,
|
|
7419
|
+
versionQuery = DEFAULT_VERSIONING_OPTIONS.versionQuery,
|
|
7420
|
+
pathPattern,
|
|
7421
|
+
acceptPattern,
|
|
7422
|
+
parseVersion
|
|
7423
|
+
} = options;
|
|
7424
|
+
let version = null;
|
|
7425
|
+
let extractedSource = null;
|
|
7426
|
+
switch (source) {
|
|
7427
|
+
case "header":
|
|
7428
|
+
version = extractFromHeader(req, versionHeader);
|
|
7429
|
+
extractedSource = version ? "header" : null;
|
|
7430
|
+
break;
|
|
7431
|
+
case "query":
|
|
7432
|
+
version = extractFromQuery(req, versionQuery);
|
|
7433
|
+
extractedSource = version ? "query" : null;
|
|
7434
|
+
break;
|
|
7435
|
+
case "path":
|
|
7436
|
+
version = extractFromPath(req, pathPattern);
|
|
7437
|
+
extractedSource = version ? "path" : null;
|
|
7438
|
+
break;
|
|
7439
|
+
case "accept":
|
|
7440
|
+
version = extractFromAccept(req, acceptPattern);
|
|
7441
|
+
extractedSource = version ? "accept" : null;
|
|
7442
|
+
break;
|
|
7443
|
+
}
|
|
7444
|
+
if (version && parseVersion) {
|
|
7445
|
+
version = parseVersion(version);
|
|
7446
|
+
}
|
|
7447
|
+
return { version, source: extractedSource };
|
|
7448
|
+
}
|
|
7449
|
+
function getVersionStatus(version, options) {
|
|
7450
|
+
const { current, supported, deprecated = [], sunset = [] } = options;
|
|
7451
|
+
if (version === current) {
|
|
7452
|
+
return "current";
|
|
7453
|
+
}
|
|
7454
|
+
if (sunset.includes(version)) {
|
|
7455
|
+
return "sunset";
|
|
7456
|
+
}
|
|
7457
|
+
if (deprecated.includes(version)) {
|
|
7458
|
+
return "deprecated";
|
|
7459
|
+
}
|
|
7460
|
+
if (supported.includes(version)) {
|
|
7461
|
+
return "supported";
|
|
7462
|
+
}
|
|
7463
|
+
return null;
|
|
7464
|
+
}
|
|
7465
|
+
function isVersionSupported(version, options) {
|
|
7466
|
+
const status = getVersionStatus(version, options);
|
|
7467
|
+
return status === "current" || status === "supported" || status === "deprecated";
|
|
7468
|
+
}
|
|
7469
|
+
function validateVersion(req, options) {
|
|
7470
|
+
const { current, supported, deprecated = [], sunset = [], sunsetDates = {} } = options;
|
|
7471
|
+
const { version, source } = extractVersion(req, options);
|
|
7472
|
+
if (!version) {
|
|
7473
|
+
return {
|
|
7474
|
+
version: current,
|
|
7475
|
+
source: null,
|
|
7476
|
+
status: "current",
|
|
7477
|
+
valid: true
|
|
7478
|
+
};
|
|
7479
|
+
}
|
|
7480
|
+
if (sunset.includes(version)) {
|
|
7481
|
+
const sunsetDate = sunsetDates[version];
|
|
7482
|
+
return {
|
|
7483
|
+
version,
|
|
7484
|
+
source,
|
|
7485
|
+
status: "sunset",
|
|
7486
|
+
valid: false,
|
|
7487
|
+
reason: `API version ${version} has been sunset${sunsetDate ? ` on ${sunsetDate.toISOString()}` : ""}`,
|
|
7488
|
+
sunsetDate
|
|
7489
|
+
};
|
|
7490
|
+
}
|
|
7491
|
+
if (deprecated.includes(version)) {
|
|
7492
|
+
const sunsetDate = sunsetDates[version];
|
|
7493
|
+
return {
|
|
7494
|
+
version,
|
|
7495
|
+
source,
|
|
7496
|
+
status: "deprecated",
|
|
7497
|
+
valid: true,
|
|
7498
|
+
sunsetDate
|
|
7499
|
+
};
|
|
7500
|
+
}
|
|
7501
|
+
if (version === current) {
|
|
7502
|
+
return {
|
|
7503
|
+
version,
|
|
7504
|
+
source,
|
|
7505
|
+
status: "current",
|
|
7506
|
+
valid: true
|
|
7507
|
+
};
|
|
7508
|
+
}
|
|
7509
|
+
if (supported.includes(version)) {
|
|
7510
|
+
return {
|
|
7511
|
+
version,
|
|
7512
|
+
source,
|
|
7513
|
+
status: "supported",
|
|
7514
|
+
valid: true
|
|
7515
|
+
};
|
|
7516
|
+
}
|
|
7517
|
+
return {
|
|
7518
|
+
version,
|
|
7519
|
+
source,
|
|
7520
|
+
status: null,
|
|
7521
|
+
valid: false,
|
|
7522
|
+
reason: `Unsupported API version: ${version}. Supported versions: ${[current, ...supported].join(", ")}`
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
function addDeprecationHeaders(response, version, sunsetDate) {
|
|
7526
|
+
const headers = new Headers(response.headers);
|
|
7527
|
+
headers.set("Deprecation", "true");
|
|
7528
|
+
if (sunsetDate) {
|
|
7529
|
+
headers.set("Sunset", sunsetDate.toUTCString());
|
|
7530
|
+
}
|
|
7531
|
+
headers.set(
|
|
7532
|
+
"Warning",
|
|
7533
|
+
`299 - "API version ${version} is deprecated${sunsetDate ? ` and will be removed on ${sunsetDate.toISOString().split("T")[0]}` : ""}"`
|
|
7534
|
+
);
|
|
7535
|
+
return new Response(response.body, {
|
|
7536
|
+
status: response.status,
|
|
7537
|
+
statusText: response.statusText,
|
|
7538
|
+
headers
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
function defaultUnsupportedResponse(version, supportedVersions) {
|
|
7542
|
+
return new Response(
|
|
7543
|
+
JSON.stringify({
|
|
7544
|
+
error: "Bad Request",
|
|
7545
|
+
message: `Unsupported API version: ${version}`,
|
|
7546
|
+
code: "UNSUPPORTED_VERSION",
|
|
7547
|
+
supportedVersions
|
|
7548
|
+
}),
|
|
7549
|
+
{
|
|
7550
|
+
status: 400,
|
|
7551
|
+
headers: { "Content-Type": "application/json" }
|
|
7552
|
+
}
|
|
7553
|
+
);
|
|
7554
|
+
}
|
|
7555
|
+
function defaultSunsetResponse(version, sunsetDate) {
|
|
7556
|
+
return new Response(
|
|
7557
|
+
JSON.stringify({
|
|
7558
|
+
error: "Gone",
|
|
7559
|
+
message: `API version ${version} is no longer available${sunsetDate ? `. It was sunset on ${sunsetDate.toISOString().split("T")[0]}` : ""}`,
|
|
7560
|
+
code: "VERSION_SUNSET"
|
|
7561
|
+
}),
|
|
7562
|
+
{
|
|
7563
|
+
status: 410,
|
|
7564
|
+
headers: { "Content-Type": "application/json" }
|
|
7565
|
+
}
|
|
7566
|
+
);
|
|
7567
|
+
}
|
|
7568
|
+
function withAPIVersion(handler, options) {
|
|
7569
|
+
return async (req, ctx) => {
|
|
7570
|
+
if (options.skip && await options.skip(req)) {
|
|
7571
|
+
return handler(req, ctx);
|
|
7572
|
+
}
|
|
7573
|
+
const result = validateVersion(req, options);
|
|
7574
|
+
if (result.status === "sunset") {
|
|
7575
|
+
return defaultSunsetResponse(result.version, result.sunsetDate);
|
|
7576
|
+
}
|
|
7577
|
+
if (!result.valid) {
|
|
7578
|
+
const onUnsupported = options.onUnsupported || ((v) => defaultUnsupportedResponse(v, [options.current, ...options.supported]));
|
|
7579
|
+
return onUnsupported(result.version);
|
|
7580
|
+
}
|
|
7581
|
+
let response = await handler(req, ctx);
|
|
7582
|
+
if (result.status === "deprecated") {
|
|
7583
|
+
if (options.onDeprecated) {
|
|
7584
|
+
options.onDeprecated(result.version, result.sunsetDate);
|
|
7585
|
+
}
|
|
7586
|
+
if (options.addDeprecationHeaders !== false) {
|
|
7587
|
+
response = addDeprecationHeaders(response, result.version, result.sunsetDate);
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
return response;
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
function createVersionRouter(handlers, options) {
|
|
7594
|
+
const versions = Object.keys(handlers);
|
|
7595
|
+
const defaultVersion = options.default || versions[versions.length - 1];
|
|
7596
|
+
return async (req, ctx) => {
|
|
7597
|
+
const { version } = extractVersion(req, {
|
|
7598
|
+
...options});
|
|
7599
|
+
const targetVersion = version || defaultVersion;
|
|
7600
|
+
const handler = handlers[targetVersion];
|
|
7601
|
+
if (!handler) {
|
|
7602
|
+
return defaultUnsupportedResponse(targetVersion, versions);
|
|
7603
|
+
}
|
|
7604
|
+
return handler(req, ctx);
|
|
7605
|
+
};
|
|
7606
|
+
}
|
|
7607
|
+
function compareVersions(a, b) {
|
|
7608
|
+
const partsA = a.split(".").map(Number);
|
|
7609
|
+
const partsB = b.split(".").map(Number);
|
|
7610
|
+
const maxLength = Math.max(partsA.length, partsB.length);
|
|
7611
|
+
for (let i = 0; i < maxLength; i++) {
|
|
7612
|
+
const numA = partsA[i] || 0;
|
|
7613
|
+
const numB = partsB[i] || 0;
|
|
7614
|
+
if (numA > numB) return 1;
|
|
7615
|
+
if (numA < numB) return -1;
|
|
7616
|
+
}
|
|
7617
|
+
return 0;
|
|
7618
|
+
}
|
|
7619
|
+
function normalizeVersion(version) {
|
|
7620
|
+
version = version.replace(/^v/i, "");
|
|
7621
|
+
const parts = version.split(".");
|
|
7622
|
+
while (parts.length < 2) {
|
|
7623
|
+
parts.push("0");
|
|
7624
|
+
}
|
|
7625
|
+
return parts.join(".");
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7628
|
+
// src/middleware/api/idempotency.ts
|
|
7629
|
+
var DEFAULT_IDEMPOTENCY_OPTIONS = {
|
|
7630
|
+
keyHeader: "idempotency-key",
|
|
7631
|
+
ttl: 864e5,
|
|
7632
|
+
// 24 hours
|
|
7633
|
+
required: false,
|
|
7634
|
+
methods: ["POST", "PUT", "PATCH"],
|
|
7635
|
+
minKeyLength: 16,
|
|
7636
|
+
maxKeyLength: 256,
|
|
7637
|
+
hashRequestBody: true,
|
|
7638
|
+
lockTimeout: 3e4,
|
|
7639
|
+
// 30 seconds
|
|
7640
|
+
waitForLock: true,
|
|
7641
|
+
maxWaitTime: 1e4
|
|
7642
|
+
// 10 seconds
|
|
7643
|
+
};
|
|
7644
|
+
var MemoryIdempotencyStore = class {
|
|
7645
|
+
cache = /* @__PURE__ */ new Map();
|
|
7646
|
+
processing = /* @__PURE__ */ new Map();
|
|
7647
|
+
maxSize;
|
|
7648
|
+
cleanupInterval = null;
|
|
7649
|
+
constructor(options = {}) {
|
|
7650
|
+
const { maxSize = 1e4, autoCleanup = true, cleanupIntervalMs = 6e4 } = options;
|
|
7651
|
+
this.maxSize = maxSize;
|
|
7652
|
+
if (autoCleanup) {
|
|
7653
|
+
this.cleanupInterval = setInterval(() => {
|
|
7654
|
+
this.cleanup();
|
|
7655
|
+
}, cleanupIntervalMs);
|
|
7656
|
+
if (this.cleanupInterval.unref) {
|
|
7657
|
+
this.cleanupInterval.unref();
|
|
7658
|
+
}
|
|
7659
|
+
}
|
|
7660
|
+
}
|
|
7661
|
+
async get(key) {
|
|
7662
|
+
const entry = this.cache.get(key);
|
|
7663
|
+
if (!entry) {
|
|
7664
|
+
return null;
|
|
7665
|
+
}
|
|
7666
|
+
if (Date.now() > entry.expiresAt) {
|
|
7667
|
+
this.cache.delete(key);
|
|
7668
|
+
return null;
|
|
7669
|
+
}
|
|
7670
|
+
return entry.response;
|
|
7671
|
+
}
|
|
7672
|
+
async set(key, response, ttl) {
|
|
7673
|
+
if (this.cache.size >= this.maxSize) {
|
|
7674
|
+
this.evictOldest();
|
|
7675
|
+
}
|
|
7676
|
+
this.cache.set(key, {
|
|
7677
|
+
response,
|
|
7678
|
+
expiresAt: Date.now() + ttl
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7681
|
+
async isProcessing(key) {
|
|
7682
|
+
const entry = this.processing.get(key);
|
|
7683
|
+
if (!entry) {
|
|
7684
|
+
return false;
|
|
7685
|
+
}
|
|
7686
|
+
if (Date.now() > entry.expiresAt) {
|
|
7687
|
+
this.processing.delete(key);
|
|
7688
|
+
return false;
|
|
7689
|
+
}
|
|
7690
|
+
return true;
|
|
7691
|
+
}
|
|
7692
|
+
async startProcessing(key, timeout) {
|
|
7693
|
+
if (await this.isProcessing(key)) {
|
|
7694
|
+
return false;
|
|
7695
|
+
}
|
|
7696
|
+
const now = Date.now();
|
|
7697
|
+
this.processing.set(key, {
|
|
7698
|
+
startedAt: now,
|
|
7699
|
+
expiresAt: now + timeout
|
|
7700
|
+
});
|
|
7701
|
+
return true;
|
|
7702
|
+
}
|
|
7703
|
+
async endProcessing(key) {
|
|
7704
|
+
this.processing.delete(key);
|
|
7705
|
+
}
|
|
7706
|
+
async delete(key) {
|
|
7707
|
+
this.cache.delete(key);
|
|
7708
|
+
this.processing.delete(key);
|
|
7709
|
+
}
|
|
7710
|
+
async cleanup() {
|
|
7711
|
+
const now = Date.now();
|
|
7712
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
7713
|
+
if (now > entry.expiresAt) {
|
|
7714
|
+
this.cache.delete(key);
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
for (const [key, entry] of this.processing.entries()) {
|
|
7718
|
+
if (now > entry.expiresAt) {
|
|
7719
|
+
this.processing.delete(key);
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
}
|
|
7723
|
+
getStats() {
|
|
7724
|
+
return {
|
|
7725
|
+
cacheSize: this.cache.size,
|
|
7726
|
+
processingSize: this.processing.size
|
|
7727
|
+
};
|
|
7728
|
+
}
|
|
7729
|
+
evictOldest() {
|
|
7730
|
+
const toRemove = Math.ceil(this.maxSize * 0.1);
|
|
7731
|
+
const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].response.cachedAt - b[1].response.cachedAt).slice(0, toRemove);
|
|
7732
|
+
for (const [key] of entries) {
|
|
7733
|
+
this.cache.delete(key);
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
/**
|
|
7737
|
+
* Clear all entries
|
|
7738
|
+
*/
|
|
7739
|
+
clear() {
|
|
7740
|
+
this.cache.clear();
|
|
7741
|
+
this.processing.clear();
|
|
7742
|
+
}
|
|
7743
|
+
/**
|
|
7744
|
+
* Stop auto cleanup
|
|
7745
|
+
*/
|
|
7746
|
+
destroy() {
|
|
7747
|
+
if (this.cleanupInterval) {
|
|
7748
|
+
clearInterval(this.cleanupInterval);
|
|
7749
|
+
this.cleanupInterval = null;
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
};
|
|
7753
|
+
var globalIdempotencyStore = null;
|
|
7754
|
+
function getGlobalIdempotencyStore() {
|
|
7755
|
+
if (!globalIdempotencyStore) {
|
|
7756
|
+
globalIdempotencyStore = new MemoryIdempotencyStore();
|
|
7757
|
+
}
|
|
7758
|
+
return globalIdempotencyStore;
|
|
7759
|
+
}
|
|
7760
|
+
function generateIdempotencyKey(length = 32) {
|
|
7761
|
+
const bytes = new Uint8Array(length);
|
|
7762
|
+
crypto.getRandomValues(bytes);
|
|
7763
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7764
|
+
}
|
|
7765
|
+
async function hashRequestBody(body) {
|
|
7766
|
+
const encoder3 = new TextEncoder();
|
|
7767
|
+
const data = encoder3.encode(body);
|
|
7768
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7769
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
7770
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7771
|
+
}
|
|
7772
|
+
function isValidIdempotencyKey(key, minLength = 16, maxLength = 256) {
|
|
7773
|
+
if (!key || typeof key !== "string") {
|
|
7774
|
+
return false;
|
|
7775
|
+
}
|
|
7776
|
+
if (key.length < minLength || key.length > maxLength) {
|
|
7777
|
+
return false;
|
|
7778
|
+
}
|
|
7779
|
+
return /^[a-zA-Z0-9_-]+$/.test(key);
|
|
7780
|
+
}
|
|
7781
|
+
async function createCacheKey(idempotencyKey, req, hashBody = true) {
|
|
7782
|
+
const parts = [idempotencyKey, req.method, new URL(req.url).pathname];
|
|
7783
|
+
if (hashBody) {
|
|
7784
|
+
try {
|
|
7785
|
+
const cloned = req.clone();
|
|
7786
|
+
const body = await cloned.text();
|
|
7787
|
+
if (body) {
|
|
7788
|
+
const bodyHash = await hashRequestBody(body);
|
|
7789
|
+
parts.push(bodyHash);
|
|
7790
|
+
}
|
|
7791
|
+
} catch {
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
return parts.join(":");
|
|
7795
|
+
}
|
|
7796
|
+
async function checkIdempotency(req, options = {}) {
|
|
7797
|
+
const {
|
|
7798
|
+
store = getGlobalIdempotencyStore(),
|
|
7799
|
+
keyHeader = DEFAULT_IDEMPOTENCY_OPTIONS.keyHeader,
|
|
7800
|
+
required = DEFAULT_IDEMPOTENCY_OPTIONS.required,
|
|
7801
|
+
methods = DEFAULT_IDEMPOTENCY_OPTIONS.methods,
|
|
7802
|
+
minKeyLength = DEFAULT_IDEMPOTENCY_OPTIONS.minKeyLength,
|
|
7803
|
+
maxKeyLength = DEFAULT_IDEMPOTENCY_OPTIONS.maxKeyLength,
|
|
7804
|
+
hashRequestBody: hashBody = DEFAULT_IDEMPOTENCY_OPTIONS.hashRequestBody,
|
|
7805
|
+
validateKey
|
|
7806
|
+
} = options;
|
|
7807
|
+
const method = req.method.toUpperCase();
|
|
7808
|
+
if (!methods.includes(method)) {
|
|
7809
|
+
return {
|
|
7810
|
+
key: null,
|
|
7811
|
+
fromCache: false,
|
|
7812
|
+
isProcessing: false
|
|
7813
|
+
};
|
|
7814
|
+
}
|
|
7815
|
+
const key = req.headers.get(keyHeader);
|
|
7816
|
+
if (!key) {
|
|
7817
|
+
if (required) {
|
|
7818
|
+
return {
|
|
7819
|
+
key: null,
|
|
7820
|
+
fromCache: false,
|
|
7821
|
+
isProcessing: false,
|
|
7822
|
+
reason: "Missing idempotency key"
|
|
7823
|
+
};
|
|
7824
|
+
}
|
|
7825
|
+
return {
|
|
7826
|
+
key: null,
|
|
7827
|
+
fromCache: false,
|
|
7828
|
+
isProcessing: false
|
|
7829
|
+
};
|
|
7830
|
+
}
|
|
7831
|
+
if (!isValidIdempotencyKey(key, minKeyLength, maxKeyLength)) {
|
|
7832
|
+
return {
|
|
7833
|
+
key,
|
|
7834
|
+
fromCache: false,
|
|
7835
|
+
isProcessing: false,
|
|
7836
|
+
reason: `Invalid idempotency key format (length must be ${minKeyLength}-${maxKeyLength}, alphanumeric)`
|
|
7837
|
+
};
|
|
7838
|
+
}
|
|
7839
|
+
if (validateKey) {
|
|
7840
|
+
const isValid = await validateKey(key);
|
|
7841
|
+
if (!isValid) {
|
|
7842
|
+
return {
|
|
7843
|
+
key,
|
|
7844
|
+
fromCache: false,
|
|
7845
|
+
isProcessing: false,
|
|
7846
|
+
reason: "Idempotency key failed custom validation"
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
const cacheKey = await createCacheKey(key, req, hashBody);
|
|
7851
|
+
const cached = await store.get(cacheKey);
|
|
7852
|
+
if (cached) {
|
|
7853
|
+
return {
|
|
7854
|
+
key,
|
|
7855
|
+
fromCache: true,
|
|
7856
|
+
cachedResponse: cached,
|
|
7857
|
+
isProcessing: false
|
|
7858
|
+
};
|
|
7859
|
+
}
|
|
7860
|
+
const isProcessing = await store.isProcessing(cacheKey);
|
|
7861
|
+
return {
|
|
7862
|
+
key,
|
|
7863
|
+
fromCache: false,
|
|
7864
|
+
isProcessing,
|
|
7865
|
+
reason: isProcessing ? "Request is currently being processed" : void 0
|
|
7866
|
+
};
|
|
7867
|
+
}
|
|
7868
|
+
async function cacheResponse(key, req, response, options = {}) {
|
|
7869
|
+
const {
|
|
7870
|
+
store = getGlobalIdempotencyStore(),
|
|
7871
|
+
ttl = DEFAULT_IDEMPOTENCY_OPTIONS.ttl,
|
|
7872
|
+
hashRequestBody: hashBody = DEFAULT_IDEMPOTENCY_OPTIONS.hashRequestBody
|
|
7873
|
+
} = options;
|
|
7874
|
+
const cacheKey = await createCacheKey(key, req, hashBody);
|
|
7875
|
+
const cloned = response.clone();
|
|
7876
|
+
const body = await cloned.text();
|
|
7877
|
+
const headers = {};
|
|
7878
|
+
response.headers.forEach((value, name) => {
|
|
7879
|
+
headers[name] = value;
|
|
7880
|
+
});
|
|
7881
|
+
const cachedResponse = {
|
|
7882
|
+
status: response.status,
|
|
7883
|
+
headers,
|
|
7884
|
+
body,
|
|
7885
|
+
cachedAt: Date.now()
|
|
7886
|
+
};
|
|
7887
|
+
await store.set(cacheKey, cachedResponse, ttl);
|
|
7888
|
+
await store.endProcessing(cacheKey);
|
|
7889
|
+
}
|
|
7890
|
+
function createResponseFromCache(cached) {
|
|
7891
|
+
const headers = new Headers(cached.headers);
|
|
7892
|
+
headers.set("x-idempotency-replayed", "true");
|
|
7893
|
+
headers.set("x-idempotency-cached-at", new Date(cached.cachedAt).toISOString());
|
|
7894
|
+
return new Response(cached.body, {
|
|
7895
|
+
status: cached.status,
|
|
7896
|
+
headers
|
|
7897
|
+
});
|
|
7898
|
+
}
|
|
7899
|
+
function defaultErrorResponse3(reason) {
|
|
7900
|
+
return new Response(
|
|
7901
|
+
JSON.stringify({
|
|
7902
|
+
error: "Bad Request",
|
|
7903
|
+
message: reason,
|
|
7904
|
+
code: "IDEMPOTENCY_ERROR"
|
|
7905
|
+
}),
|
|
7906
|
+
{
|
|
7907
|
+
status: 400,
|
|
7908
|
+
headers: { "Content-Type": "application/json" }
|
|
7909
|
+
}
|
|
7910
|
+
);
|
|
7911
|
+
}
|
|
7912
|
+
async function waitForLock(store, cacheKey, maxWaitTime, pollInterval = 100) {
|
|
7913
|
+
const startTime = Date.now();
|
|
7914
|
+
while (Date.now() - startTime < maxWaitTime) {
|
|
7915
|
+
const cached = await store.get(cacheKey);
|
|
7916
|
+
if (cached) {
|
|
7917
|
+
return cached;
|
|
7918
|
+
}
|
|
7919
|
+
const isProcessing = await store.isProcessing(cacheKey);
|
|
7920
|
+
if (!isProcessing) {
|
|
7921
|
+
return null;
|
|
7922
|
+
}
|
|
7923
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
7924
|
+
}
|
|
7925
|
+
return null;
|
|
7926
|
+
}
|
|
7927
|
+
function withIdempotency(handler, options = {}) {
|
|
7928
|
+
return async (req, ctx) => {
|
|
7929
|
+
if (options.skip && await options.skip(req)) {
|
|
7930
|
+
return handler(req, ctx);
|
|
7931
|
+
}
|
|
7932
|
+
const {
|
|
7933
|
+
store = getGlobalIdempotencyStore(),
|
|
7934
|
+
methods = DEFAULT_IDEMPOTENCY_OPTIONS.methods,
|
|
7935
|
+
hashRequestBody: hashBody = DEFAULT_IDEMPOTENCY_OPTIONS.hashRequestBody,
|
|
7936
|
+
lockTimeout = DEFAULT_IDEMPOTENCY_OPTIONS.lockTimeout,
|
|
7937
|
+
waitForLock: shouldWait = DEFAULT_IDEMPOTENCY_OPTIONS.waitForLock,
|
|
7938
|
+
maxWaitTime = DEFAULT_IDEMPOTENCY_OPTIONS.maxWaitTime,
|
|
7939
|
+
onError,
|
|
7940
|
+
onCacheHit
|
|
7941
|
+
} = options;
|
|
7942
|
+
const method = req.method.toUpperCase();
|
|
7943
|
+
if (!methods.includes(method)) {
|
|
7944
|
+
return handler(req, ctx);
|
|
7945
|
+
}
|
|
7946
|
+
const result = await checkIdempotency(req, options);
|
|
7947
|
+
if (result.reason && !result.isProcessing) {
|
|
7948
|
+
const errorHandler = onError || defaultErrorResponse3;
|
|
7949
|
+
return errorHandler(result.reason);
|
|
7950
|
+
}
|
|
7951
|
+
if (result.fromCache && result.cachedResponse) {
|
|
7952
|
+
if (onCacheHit) {
|
|
7953
|
+
onCacheHit(result.key, result.cachedResponse);
|
|
7954
|
+
}
|
|
7955
|
+
return createResponseFromCache(result.cachedResponse);
|
|
7956
|
+
}
|
|
7957
|
+
if (!result.key) {
|
|
7958
|
+
return handler(req, ctx);
|
|
7959
|
+
}
|
|
7960
|
+
const cacheKey = await createCacheKey(result.key, req, hashBody);
|
|
7961
|
+
if (result.isProcessing) {
|
|
7962
|
+
if (shouldWait) {
|
|
7963
|
+
const cached = await waitForLock(store, cacheKey, maxWaitTime);
|
|
7964
|
+
if (cached) {
|
|
7965
|
+
if (onCacheHit) {
|
|
7966
|
+
onCacheHit(result.key, cached);
|
|
7967
|
+
}
|
|
7968
|
+
return createResponseFromCache(cached);
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
return new Response(
|
|
7972
|
+
JSON.stringify({
|
|
7973
|
+
error: "Conflict",
|
|
7974
|
+
message: "Request with this idempotency key is currently being processed",
|
|
7975
|
+
code: "IDEMPOTENCY_CONFLICT"
|
|
7976
|
+
}),
|
|
7977
|
+
{
|
|
7978
|
+
status: 409,
|
|
7979
|
+
headers: { "Content-Type": "application/json" }
|
|
7980
|
+
}
|
|
7981
|
+
);
|
|
7982
|
+
}
|
|
7983
|
+
const acquired = await store.startProcessing(cacheKey, lockTimeout);
|
|
7984
|
+
if (!acquired) {
|
|
7985
|
+
if (shouldWait) {
|
|
7986
|
+
const cached = await waitForLock(store, cacheKey, maxWaitTime);
|
|
7987
|
+
if (cached) {
|
|
7988
|
+
if (onCacheHit) {
|
|
7989
|
+
onCacheHit(result.key, cached);
|
|
7990
|
+
}
|
|
7991
|
+
return createResponseFromCache(cached);
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
return new Response(
|
|
7995
|
+
JSON.stringify({
|
|
7996
|
+
error: "Conflict",
|
|
7997
|
+
message: "Request with this idempotency key is currently being processed",
|
|
7998
|
+
code: "IDEMPOTENCY_CONFLICT"
|
|
7999
|
+
}),
|
|
8000
|
+
{
|
|
8001
|
+
status: 409,
|
|
8002
|
+
headers: { "Content-Type": "application/json" }
|
|
8003
|
+
}
|
|
8004
|
+
);
|
|
8005
|
+
}
|
|
8006
|
+
try {
|
|
8007
|
+
const response = await handler(req, ctx);
|
|
8008
|
+
if (response.status >= 200 && response.status < 300) {
|
|
8009
|
+
await cacheResponse(result.key, req, response, options);
|
|
8010
|
+
} else {
|
|
8011
|
+
await store.endProcessing(cacheKey);
|
|
8012
|
+
}
|
|
8013
|
+
return response;
|
|
8014
|
+
} catch (error) {
|
|
8015
|
+
await store.endProcessing(cacheKey);
|
|
8016
|
+
throw error;
|
|
8017
|
+
}
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
function addIdempotencyHeader(headers = {}, options = {}) {
|
|
8021
|
+
const { headerName = "idempotency-key", key = generateIdempotencyKey() } = options;
|
|
8022
|
+
return {
|
|
8023
|
+
...headers,
|
|
8024
|
+
[headerName]: key
|
|
8025
|
+
};
|
|
8026
|
+
}
|
|
8027
|
+
|
|
8028
|
+
// src/middleware/api/middleware.ts
|
|
8029
|
+
async function checkAPIProtection(req, options) {
|
|
8030
|
+
const result = {
|
|
8031
|
+
passed: true
|
|
8032
|
+
};
|
|
8033
|
+
if (options.timestamp !== false) {
|
|
8034
|
+
const timestampResult = validateTimestamp(req, options.timestamp);
|
|
8035
|
+
result.timestamp = timestampResult;
|
|
8036
|
+
if (!timestampResult.valid) {
|
|
8037
|
+
result.passed = false;
|
|
8038
|
+
result.error = {
|
|
8039
|
+
type: "timestamp",
|
|
8040
|
+
message: timestampResult.reason || "Invalid timestamp"
|
|
8041
|
+
};
|
|
8042
|
+
return result;
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
if (options.replay !== false) {
|
|
8046
|
+
const replayOpts = options.replay;
|
|
8047
|
+
const replayResult = await checkReplay(req, {
|
|
8048
|
+
...replayOpts,
|
|
8049
|
+
store: replayOpts.store || getGlobalNonceStore()
|
|
8050
|
+
});
|
|
8051
|
+
result.replay = replayResult;
|
|
8052
|
+
if (replayResult.isReplay) {
|
|
8053
|
+
result.passed = false;
|
|
8054
|
+
result.error = {
|
|
8055
|
+
type: "replay",
|
|
8056
|
+
message: "Request replay detected",
|
|
8057
|
+
details: { nonce: replayResult.nonce }
|
|
8058
|
+
};
|
|
8059
|
+
return result;
|
|
8060
|
+
}
|
|
8061
|
+
if (replayResult.reason && replayOpts.required !== false) {
|
|
8062
|
+
result.passed = false;
|
|
8063
|
+
result.error = {
|
|
8064
|
+
type: "replay",
|
|
8065
|
+
message: replayResult.reason
|
|
8066
|
+
};
|
|
8067
|
+
return result;
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
if (options.signing !== false) {
|
|
8071
|
+
const signingOpts = options.signing;
|
|
8072
|
+
const signingResult = await verifySignature(req, signingOpts);
|
|
8073
|
+
result.signing = signingResult;
|
|
8074
|
+
if (!signingResult.valid) {
|
|
8075
|
+
result.passed = false;
|
|
8076
|
+
result.error = {
|
|
8077
|
+
type: "signing",
|
|
8078
|
+
message: signingResult.reason || "Invalid signature"
|
|
8079
|
+
};
|
|
8080
|
+
return result;
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
if (options.versioning !== false) {
|
|
8084
|
+
const versioningOpts = options.versioning;
|
|
8085
|
+
const versionResult = validateVersion(req, versioningOpts);
|
|
8086
|
+
result.version = versionResult;
|
|
8087
|
+
if (!versionResult.valid) {
|
|
8088
|
+
result.passed = false;
|
|
8089
|
+
result.error = {
|
|
8090
|
+
type: "versioning",
|
|
8091
|
+
message: versionResult.reason || "Unsupported version",
|
|
8092
|
+
details: { version: versionResult.version }
|
|
8093
|
+
};
|
|
8094
|
+
return result;
|
|
8095
|
+
}
|
|
8096
|
+
}
|
|
8097
|
+
if (options.idempotency !== false) {
|
|
8098
|
+
const idempotencyOpts = options.idempotency;
|
|
8099
|
+
const idempotencyResult = await checkIdempotency(req, {
|
|
8100
|
+
...idempotencyOpts,
|
|
8101
|
+
store: idempotencyOpts.store || getGlobalIdempotencyStore()
|
|
8102
|
+
});
|
|
8103
|
+
result.idempotency = idempotencyResult;
|
|
8104
|
+
if (idempotencyResult.reason && !idempotencyResult.isProcessing && !idempotencyResult.fromCache) {
|
|
8105
|
+
if (idempotencyOpts.required) {
|
|
8106
|
+
result.passed = false;
|
|
8107
|
+
result.error = {
|
|
8108
|
+
type: "idempotency",
|
|
8109
|
+
message: idempotencyResult.reason,
|
|
8110
|
+
details: { key: idempotencyResult.key }
|
|
8111
|
+
};
|
|
8112
|
+
return result;
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
return result;
|
|
8117
|
+
}
|
|
8118
|
+
function defaultErrorResponse4(error) {
|
|
8119
|
+
const statusMap = {
|
|
8120
|
+
signing: 401,
|
|
8121
|
+
replay: 403,
|
|
8122
|
+
timestamp: 400,
|
|
8123
|
+
versioning: 400,
|
|
8124
|
+
idempotency: 400
|
|
8125
|
+
};
|
|
8126
|
+
const codeMap = {
|
|
8127
|
+
signing: "INVALID_SIGNATURE",
|
|
8128
|
+
replay: "REPLAY_DETECTED",
|
|
8129
|
+
timestamp: "INVALID_TIMESTAMP",
|
|
8130
|
+
versioning: "UNSUPPORTED_VERSION",
|
|
8131
|
+
idempotency: "IDEMPOTENCY_ERROR"
|
|
8132
|
+
};
|
|
8133
|
+
return new Response(
|
|
8134
|
+
JSON.stringify({
|
|
8135
|
+
error: error.type === "signing" ? "Unauthorized" : error.type === "replay" ? "Forbidden" : "Bad Request",
|
|
8136
|
+
message: error.message,
|
|
8137
|
+
code: codeMap[error.type],
|
|
8138
|
+
...error.details || {}
|
|
8139
|
+
}),
|
|
8140
|
+
{
|
|
8141
|
+
status: statusMap[error.type],
|
|
8142
|
+
headers: { "Content-Type": "application/json" }
|
|
8143
|
+
}
|
|
8144
|
+
);
|
|
8145
|
+
}
|
|
8146
|
+
function withAPIProtection(handler, options) {
|
|
8147
|
+
return async (req, ctx) => {
|
|
8148
|
+
if (options.skip && await options.skip(req)) {
|
|
8149
|
+
return handler(req, ctx);
|
|
8150
|
+
}
|
|
8151
|
+
const result = await checkAPIProtection(req, options);
|
|
8152
|
+
if (!result.passed && result.error) {
|
|
8153
|
+
const onError = options.onError || defaultErrorResponse4;
|
|
8154
|
+
return onError(result.error);
|
|
8155
|
+
}
|
|
8156
|
+
if (result.idempotency?.fromCache && result.idempotency.cachedResponse) {
|
|
8157
|
+
const idempotencyOpts = options.idempotency;
|
|
8158
|
+
if (idempotencyOpts.onCacheHit) {
|
|
8159
|
+
idempotencyOpts.onCacheHit(result.idempotency.key, result.idempotency.cachedResponse);
|
|
8160
|
+
}
|
|
8161
|
+
return createResponseFromCache(result.idempotency.cachedResponse);
|
|
8162
|
+
}
|
|
8163
|
+
if (result.idempotency?.isProcessing) {
|
|
8164
|
+
return new Response(
|
|
8165
|
+
JSON.stringify({
|
|
8166
|
+
error: "Conflict",
|
|
8167
|
+
message: "Request with this idempotency key is currently being processed",
|
|
8168
|
+
code: "IDEMPOTENCY_CONFLICT"
|
|
8169
|
+
}),
|
|
8170
|
+
{
|
|
8171
|
+
status: 409,
|
|
8172
|
+
headers: { "Content-Type": "application/json" }
|
|
8173
|
+
}
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
let response = await handler(req, ctx);
|
|
8177
|
+
if (result.version?.status === "deprecated") {
|
|
8178
|
+
const versioningOpts = options.versioning;
|
|
8179
|
+
if (versioningOpts.addDeprecationHeaders !== false) {
|
|
8180
|
+
response = addDeprecationHeaders(response, result.version.version, result.version.sunsetDate);
|
|
8181
|
+
}
|
|
8182
|
+
if (versioningOpts.onDeprecated) {
|
|
8183
|
+
versioningOpts.onDeprecated(result.version.version, result.version.sunsetDate);
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
if (result.idempotency?.key && options.idempotency !== false) {
|
|
8187
|
+
const idempotencyOpts = options.idempotency;
|
|
8188
|
+
if (response.status >= 200 && response.status < 300) {
|
|
8189
|
+
await cacheResponse(result.idempotency.key, req, response, idempotencyOpts);
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
return response;
|
|
8193
|
+
};
|
|
8194
|
+
}
|
|
8195
|
+
function withAPIProtectionPreset(handler, preset, overrides = {}) {
|
|
8196
|
+
const presetConfig = API_PROTECTION_PRESETS[preset];
|
|
8197
|
+
const mergedOptions = {
|
|
8198
|
+
...presetConfig,
|
|
8199
|
+
...overrides
|
|
8200
|
+
};
|
|
8201
|
+
if (presetConfig.signing && typeof presetConfig.signing === "object" && overrides.signing && typeof overrides.signing === "object") {
|
|
8202
|
+
mergedOptions.signing = { ...presetConfig.signing, ...overrides.signing };
|
|
8203
|
+
}
|
|
8204
|
+
if (presetConfig.replay && typeof presetConfig.replay === "object" && overrides.replay && typeof overrides.replay === "object") {
|
|
8205
|
+
mergedOptions.replay = { ...presetConfig.replay, ...overrides.replay };
|
|
8206
|
+
}
|
|
8207
|
+
if (presetConfig.timestamp && typeof presetConfig.timestamp === "object" && overrides.timestamp && typeof overrides.timestamp === "object") {
|
|
8208
|
+
mergedOptions.timestamp = { ...presetConfig.timestamp, ...overrides.timestamp };
|
|
8209
|
+
}
|
|
8210
|
+
if (presetConfig.idempotency && typeof presetConfig.idempotency === "object" && overrides.idempotency && typeof overrides.idempotency === "object") {
|
|
8211
|
+
mergedOptions.idempotency = { ...presetConfig.idempotency, ...overrides.idempotency };
|
|
8212
|
+
}
|
|
8213
|
+
return withAPIProtection(handler, mergedOptions);
|
|
8214
|
+
}
|
|
8215
|
+
|
|
6587
8216
|
// src/index.ts
|
|
6588
|
-
var VERSION = "0.
|
|
8217
|
+
var VERSION = "0.8.0";
|
|
6589
8218
|
|
|
8219
|
+
exports.API_PROTECTION_PRESETS = API_PROTECTION_PRESETS;
|
|
6590
8220
|
exports.AuditMemoryStore = MemoryStore2;
|
|
6591
8221
|
exports.AuthenticationError = AuthenticationError;
|
|
6592
8222
|
exports.AuthorizationError = AuthorizationError;
|
|
@@ -6605,6 +8235,8 @@ exports.JSONFormatter = JSONFormatter;
|
|
|
6605
8235
|
exports.KNOWN_BOT_PATTERNS = KNOWN_BOT_PATTERNS;
|
|
6606
8236
|
exports.MIME_TYPES = MIME_TYPES;
|
|
6607
8237
|
exports.MemoryBehaviorStore = MemoryBehaviorStore;
|
|
8238
|
+
exports.MemoryIdempotencyStore = MemoryIdempotencyStore;
|
|
8239
|
+
exports.MemoryNonceStore = MemoryNonceStore;
|
|
6608
8240
|
exports.MemoryStore = MemoryStore;
|
|
6609
8241
|
exports.MultiStore = MultiStore;
|
|
6610
8242
|
exports.PRESET_API = PRESET_API;
|
|
@@ -6617,17 +8249,27 @@ exports.StructuredFormatter = StructuredFormatter;
|
|
|
6617
8249
|
exports.TextFormatter = TextFormatter;
|
|
6618
8250
|
exports.VERSION = VERSION;
|
|
6619
8251
|
exports.ValidationError = ValidationError;
|
|
8252
|
+
exports.addDeprecationHeaders = addDeprecationHeaders;
|
|
8253
|
+
exports.addIdempotencyHeader = addIdempotencyHeader;
|
|
8254
|
+
exports.addNonceHeader = addNonceHeader;
|
|
8255
|
+
exports.addTimestampHeader = addTimestampHeader;
|
|
6620
8256
|
exports.analyzeBehavior = analyzeBehavior;
|
|
6621
8257
|
exports.analyzeUserAgent = analyzeUserAgent;
|
|
6622
8258
|
exports.anonymizeIp = anonymizeIp;
|
|
6623
8259
|
exports.buildCSP = buildCSP;
|
|
8260
|
+
exports.buildCanonicalString = buildCanonicalString;
|
|
6624
8261
|
exports.buildHSTS = buildHSTS;
|
|
6625
8262
|
exports.buildPermissionsPolicy = buildPermissionsPolicy;
|
|
8263
|
+
exports.cacheResponse = cacheResponse;
|
|
8264
|
+
exports.checkAPIProtection = checkAPIProtection;
|
|
6626
8265
|
exports.checkBehavior = checkBehavior;
|
|
6627
8266
|
exports.checkCaptcha = checkCaptcha;
|
|
6628
8267
|
exports.checkHoneypot = checkHoneypot;
|
|
8268
|
+
exports.checkIdempotency = checkIdempotency;
|
|
6629
8269
|
exports.checkRateLimit = checkRateLimit;
|
|
8270
|
+
exports.checkReplay = checkReplay;
|
|
6630
8271
|
exports.clearAllRateLimits = clearAllRateLimits;
|
|
8272
|
+
exports.compareVersions = compareVersions;
|
|
6631
8273
|
exports.createAuditMemoryStore = createMemoryStore2;
|
|
6632
8274
|
exports.createAuditMiddleware = createAuditMiddleware;
|
|
6633
8275
|
exports.createBotPattern = createBotPattern;
|
|
@@ -6636,17 +8278,20 @@ exports.createCSRFToken = createToken;
|
|
|
6636
8278
|
exports.createConsoleStore = createConsoleStore;
|
|
6637
8279
|
exports.createDatadogStore = createDatadogStore;
|
|
6638
8280
|
exports.createExternalStore = createExternalStore;
|
|
8281
|
+
exports.createHMAC = createHMAC;
|
|
6639
8282
|
exports.createJSONFormatter = createJSONFormatter;
|
|
6640
8283
|
exports.createMemoryStore = createMemoryStore;
|
|
6641
8284
|
exports.createMultiStore = createMultiStore;
|
|
6642
8285
|
exports.createRateLimiter = createRateLimiter;
|
|
6643
8286
|
exports.createRedactor = createRedactor;
|
|
8287
|
+
exports.createResponseFromCache = createResponseFromCache;
|
|
6644
8288
|
exports.createSecurityHeaders = createSecurityHeaders;
|
|
6645
8289
|
exports.createSecurityHeadersObject = createSecurityHeadersObject;
|
|
6646
8290
|
exports.createSecurityTracker = createSecurityTracker;
|
|
6647
8291
|
exports.createStructuredFormatter = createStructuredFormatter;
|
|
6648
8292
|
exports.createTextFormatter = createTextFormatter;
|
|
6649
8293
|
exports.createValidator = createValidator;
|
|
8294
|
+
exports.createVersionRouter = createVersionRouter;
|
|
6650
8295
|
exports.decodeJWT = decodeJWT;
|
|
6651
8296
|
exports.detectBot = detectBot;
|
|
6652
8297
|
exports.detectFileType = detectFileType;
|
|
@@ -6654,23 +8299,34 @@ exports.detectSQLInjection = detectSQLInjection;
|
|
|
6654
8299
|
exports.detectXSS = detectXSS;
|
|
6655
8300
|
exports.escapeHtml = escapeHtml;
|
|
6656
8301
|
exports.extractBearerToken = extractBearerToken;
|
|
8302
|
+
exports.extractVersion = extractVersion;
|
|
6657
8303
|
exports.formatDuration = formatDuration;
|
|
8304
|
+
exports.formatTimestamp = formatTimestamp;
|
|
6658
8305
|
exports.generateCSRF = generateCSRF;
|
|
6659
8306
|
exports.generateHCaptcha = generateHCaptcha;
|
|
6660
8307
|
exports.generateHoneypotCSS = generateHoneypotCSS;
|
|
6661
8308
|
exports.generateHoneypotHTML = generateHoneypotHTML;
|
|
8309
|
+
exports.generateIdempotencyKey = generateIdempotencyKey;
|
|
8310
|
+
exports.generateNonce = generateNonce2;
|
|
6662
8311
|
exports.generateRecaptchaV2 = generateRecaptchaV2;
|
|
6663
8312
|
exports.generateRecaptchaV3 = generateRecaptchaV3;
|
|
8313
|
+
exports.generateSignature = generateSignature;
|
|
8314
|
+
exports.generateSignatureHeaders = generateSignatureHeaders;
|
|
6664
8315
|
exports.generateTurnstile = generateTurnstile;
|
|
6665
8316
|
exports.getBotsByCategory = getBotsByCategory;
|
|
6666
8317
|
exports.getClientIp = getClientIp;
|
|
6667
8318
|
exports.getGeoInfo = getGeoInfo;
|
|
6668
8319
|
exports.getGlobalBehaviorStore = getGlobalBehaviorStore;
|
|
8320
|
+
exports.getGlobalIdempotencyStore = getGlobalIdempotencyStore;
|
|
6669
8321
|
exports.getGlobalMemoryStore = getGlobalMemoryStore;
|
|
8322
|
+
exports.getGlobalNonceStore = getGlobalNonceStore;
|
|
6670
8323
|
exports.getPreset = getPreset;
|
|
6671
8324
|
exports.getRateLimitStatus = getRateLimitStatus;
|
|
8325
|
+
exports.getRequestAge = getRequestAge;
|
|
8326
|
+
exports.getVersionStatus = getVersionStatus;
|
|
6672
8327
|
exports.hasPathTraversal = hasPathTraversal;
|
|
6673
8328
|
exports.hasSQLInjection = hasSQLInjection;
|
|
8329
|
+
exports.hashRequestBody = hashRequestBody;
|
|
6674
8330
|
exports.isBotAllowed = isBotAllowed;
|
|
6675
8331
|
exports.isFormRequest = isFormRequest;
|
|
6676
8332
|
exports.isJsonRequest = isJsonRequest;
|
|
@@ -6678,12 +8334,18 @@ exports.isLocalhost = isLocalhost;
|
|
|
6678
8334
|
exports.isPrivateIp = isPrivateIp;
|
|
6679
8335
|
exports.isSecureError = isSecureError;
|
|
6680
8336
|
exports.isSuspiciousUA = isSuspiciousUA;
|
|
8337
|
+
exports.isTimestampValid = isTimestampValid;
|
|
8338
|
+
exports.isValidIdempotencyKey = isValidIdempotencyKey;
|
|
6681
8339
|
exports.isValidIp = isValidIp;
|
|
8340
|
+
exports.isValidNonceFormat = isValidNonceFormat;
|
|
8341
|
+
exports.isVersionSupported = isVersionSupported;
|
|
6682
8342
|
exports.mask = mask;
|
|
6683
8343
|
exports.normalizeIp = normalizeIp;
|
|
8344
|
+
exports.normalizeVersion = normalizeVersion;
|
|
6684
8345
|
exports.nowInMs = nowInMs;
|
|
6685
8346
|
exports.nowInSeconds = nowInSeconds;
|
|
6686
8347
|
exports.parseDuration = parseDuration;
|
|
8348
|
+
exports.parseTimestamp = parseTimestamp;
|
|
6687
8349
|
exports.redactCreditCard = redactCreditCard;
|
|
6688
8350
|
exports.redactEmail = redactEmail;
|
|
6689
8351
|
exports.redactHeaders = redactHeaders;
|
|
@@ -6700,6 +8362,7 @@ exports.sanitizePath = sanitizePath;
|
|
|
6700
8362
|
exports.sanitizeSQLInput = sanitizeSQLInput;
|
|
6701
8363
|
exports.sleep = sleep;
|
|
6702
8364
|
exports.stripHtml = stripHtml;
|
|
8365
|
+
exports.timingSafeEqual = timingSafeEqual;
|
|
6703
8366
|
exports.toSecureError = toSecureError;
|
|
6704
8367
|
exports.tokensMatch = tokensMatch;
|
|
6705
8368
|
exports.trackSecurityEvent = trackSecurityEvent;
|
|
@@ -6712,10 +8375,16 @@ exports.validateFiles = validateFiles;
|
|
|
6712
8375
|
exports.validatePath = validatePath;
|
|
6713
8376
|
exports.validateQuery = validateQuery;
|
|
6714
8377
|
exports.validateRequest = validateRequest;
|
|
8378
|
+
exports.validateTimestamp = validateTimestamp;
|
|
8379
|
+
exports.validateVersion = validateVersion;
|
|
6715
8380
|
exports.verifyCSRFToken = verifyToken;
|
|
6716
8381
|
exports.verifyCaptcha = verifyCaptcha;
|
|
6717
8382
|
exports.verifyJWT = verifyJWT;
|
|
8383
|
+
exports.verifySignature = verifySignature;
|
|
6718
8384
|
exports.withAPIKey = withAPIKey;
|
|
8385
|
+
exports.withAPIProtection = withAPIProtection;
|
|
8386
|
+
exports.withAPIProtectionPreset = withAPIProtectionPreset;
|
|
8387
|
+
exports.withAPIVersion = withAPIVersion;
|
|
6719
8388
|
exports.withAuditLog = withAuditLog;
|
|
6720
8389
|
exports.withAuth = withAuth;
|
|
6721
8390
|
exports.withBehaviorAnalysis = withBehaviorAnalysis;
|
|
@@ -6729,16 +8398,20 @@ exports.withContentType = withContentType;
|
|
|
6729
8398
|
exports.withFileValidation = withFileValidation;
|
|
6730
8399
|
exports.withHoneypot = withHoneypot;
|
|
6731
8400
|
exports.withHoneypotProtection = withHoneypotProtection;
|
|
8401
|
+
exports.withIdempotency = withIdempotency;
|
|
6732
8402
|
exports.withJWT = withJWT;
|
|
6733
8403
|
exports.withOptionalAuth = withOptionalAuth;
|
|
6734
8404
|
exports.withRateLimit = withRateLimit;
|
|
8405
|
+
exports.withReplayPrevention = withReplayPrevention;
|
|
6735
8406
|
exports.withRequestId = withRequestId;
|
|
8407
|
+
exports.withRequestSigning = withRequestSigning;
|
|
6736
8408
|
exports.withRoles = withRoles;
|
|
6737
8409
|
exports.withSQLProtection = withSQLProtection;
|
|
6738
8410
|
exports.withSanitization = withSanitization;
|
|
6739
8411
|
exports.withSecureValidation = withSecureValidation;
|
|
6740
8412
|
exports.withSecurityHeaders = withSecurityHeaders;
|
|
6741
8413
|
exports.withSession = withSession;
|
|
8414
|
+
exports.withTimestamp = withTimestamp;
|
|
6742
8415
|
exports.withTiming = withTiming;
|
|
6743
8416
|
exports.withUserAgentProtection = withUserAgentProtection;
|
|
6744
8417
|
exports.withValidation = withValidation;
|