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.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.7.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;