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