@better-auth/api-key 1.7.0-beta.5 → 1.7.0-beta.7

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/client.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-BR70O3Q3.mjs";
2
- import { i as API_KEY_ERROR_CODES, n as apiKey } from "./index-BWgvCest.mjs";
1
+ import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-CbtANSbR.mjs";
2
+ import { i as API_KEY_ERROR_CODES, n as apiKey } from "./index-BU-xUFsI.mjs";
3
3
  import * as better_auth0 from "better-auth";
4
4
 
5
5
  //#region src/client.d.ts
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DsrnkkeB.mjs";
1
+ import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DzO-nCoq.mjs";
2
2
  //#region src/client.ts
3
3
  const apiKeyClient = () => {
4
4
  return {
@@ -1,4 +1,4 @@
1
- import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-BR70O3Q3.mjs";
1
+ import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-CbtANSbR.mjs";
2
2
  import * as better_auth0 from "better-auth";
3
3
  import * as zod from "zod";
4
4
  import * as better_call0 from "better-call";
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-BR70O3Q3.mjs";
2
- import { i as API_KEY_ERROR_CODES, n as apiKey, r as defaultKeyHasher, t as API_KEY_TABLE_NAME } from "./index-BWgvCest.mjs";
1
+ import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-CbtANSbR.mjs";
2
+ import { i as API_KEY_ERROR_CODES, n as apiKey, r as defaultKeyHasher, t as API_KEY_TABLE_NAME } from "./index-BU-xUFsI.mjs";
3
3
  export { API_KEY_ERROR_CODES, API_KEY_TABLE_NAME, ApiKey, ApiKeyConfigurationOptions, ApiKeyOptions, apiKey, defaultKeyHasher };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DsrnkkeB.mjs";
1
+ import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DzO-nCoq.mjs";
2
2
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
3
3
  import { base64Url } from "@better-auth/utils/base64";
4
4
  import { createHash } from "@better-auth/utils/hash";
@@ -167,21 +167,46 @@ async function getApiKeyByIdFromStorage(ctx, id, storage) {
167
167
  return deserializeApiKey(await storage.get(key));
168
168
  }
169
169
  /**
170
+ * Serializes reference-list mutations per `refKey` within a single process.
171
+ * Each new mutation chains onto the previous one for the same key, so the
172
+ * read/modify/write below never interleaves with another mutation of the same
173
+ * list. This closes the lost-update race in secondary-storage-only mode, where
174
+ * the serialized list is the source of truth for listing.
175
+ *
176
+ * Limitation: the lock is in-process only. Across multiple server instances
177
+ * sharing one secondary storage, concurrent writers can still lose updates.
178
+ * Secondary storage with `fallbackToDatabase` avoids this by treating the list
179
+ * as an invalidate-only cache with the database as the source of truth.
180
+ * FIXME(api-key-reflist-durable): on `next`, drop the source-of-truth reference
181
+ * list entirely and make the database authoritative for listing, removing this
182
+ * in-process lock.
183
+ */
184
+ const refListLocks = /* @__PURE__ */ new Map();
185
+ function withRefListLock(refKey, task) {
186
+ const tracked = (refListLocks.get(refKey) ?? Promise.resolve()).then(task, task).finally(() => {
187
+ if (refListLocks.get(refKey) === tracked) refListLocks.delete(refKey);
188
+ });
189
+ refListLocks.set(refKey, tracked);
190
+ return tracked;
191
+ }
192
+ /**
170
193
  * Read-modify-write the ref list:
171
194
  * used only when the list is the source of truth.
172
195
  */
173
196
  async function modifyRefList(storage, refKey, modify) {
174
- const refListData = await storage.get(refKey);
175
- let keyIds = [];
176
- if (refListData && typeof refListData === "string") try {
177
- keyIds = JSON.parse(refListData);
178
- } catch {
179
- keyIds = [];
180
- }
181
- else if (Array.isArray(refListData)) keyIds = refListData;
182
- const next = modify(keyIds);
183
- if (next.length === 0) await storage.delete(refKey);
184
- else await storage.set(refKey, JSON.stringify(next));
197
+ await withRefListLock(refKey, async () => {
198
+ const refListData = await storage.get(refKey);
199
+ let keyIds = [];
200
+ if (refListData && typeof refListData === "string") try {
201
+ keyIds = JSON.parse(refListData);
202
+ } catch {
203
+ keyIds = [];
204
+ }
205
+ else if (Array.isArray(refListData)) keyIds = refListData;
206
+ const next = modify(keyIds);
207
+ if (next.length === 0) await storage.delete(refKey);
208
+ else await storage.set(refKey, JSON.stringify(next));
209
+ });
185
210
  }
186
211
  async function setApiKeyInStorage(_ctx, apiKey, storage, ttl, opts) {
187
212
  const serialized = serializeApiKey(apiKey);
@@ -852,7 +877,7 @@ function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllEx
852
877
  //#endregion
853
878
  //#region src/routes/delete-all-expired-api-keys.ts
854
879
  function deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys }) {
855
- return createAuthEndpoint({ method: "POST" }, async (ctx) => {
880
+ return createAuthEndpoint.serverOnly({ method: "POST" }, async (ctx) => {
856
881
  try {
857
882
  await deleteAllExpiredApiKeys(ctx.context, true);
858
883
  } catch (error) {
@@ -1467,7 +1492,7 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1467
1492
  } }
1468
1493
  }, async (ctx) => {
1469
1494
  const { configId, keyId, expiresIn, enabled, metadata, refillAmount, refillInterval, remaining, name, permissions, rateLimitEnabled, rateLimitTimeWindow, rateLimitMax } = ctx.body;
1470
- const session = await getSessionFromCtx(ctx);
1495
+ const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
1471
1496
  const authRequired = ctx.request || ctx.headers;
1472
1497
  const user = authRequired && !session ? null : session?.user || { id: ctx.body.userId };
1473
1498
  if (!user?.id) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
@@ -1565,69 +1590,47 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1565
1590
  //#endregion
1566
1591
  //#region src/rate-limit.ts
1567
1592
  /**
1568
- * Determines if a request is allowed based on rate limiting parameters.
1569
- *
1570
- * @returns An object indicating whether the request is allowed and, if not,
1571
- * a message and updated ApiKey data.
1593
+ * Decides how the current request affects the per-key rate-limit counter, based
1594
+ * on the read-in-memory ApiKey. The verify route applies the result atomically;
1595
+ * this function performs no writes.
1572
1596
  */
1573
- function isRateLimited(apiKey, opts) {
1597
+ function evaluateRateLimit(apiKey, opts) {
1574
1598
  const now = /* @__PURE__ */ new Date();
1575
1599
  const lastRequest = apiKey.lastRequest;
1576
1600
  const rateLimitTimeWindow = apiKey.rateLimitTimeWindow;
1577
1601
  const rateLimitMax = apiKey.rateLimitMax;
1578
- let requestCount = apiKey.requestCount;
1579
1602
  if (opts.rateLimit.enabled === false) return {
1580
- success: true,
1581
- message: null,
1582
- update: { lastRequest: now },
1583
- tryAgainIn: null
1603
+ type: "skip",
1604
+ lastRequest: now
1584
1605
  };
1585
1606
  if (apiKey.rateLimitEnabled === false) return {
1586
- success: true,
1587
- message: null,
1588
- update: { lastRequest: now },
1589
- tryAgainIn: null
1607
+ type: "skip",
1608
+ lastRequest: now
1590
1609
  };
1591
1610
  if (rateLimitTimeWindow === null || rateLimitMax === null) return {
1592
- success: true,
1593
- message: null,
1594
- update: null,
1595
- tryAgainIn: null
1611
+ type: "skip",
1612
+ lastRequest: null
1596
1613
  };
1597
1614
  if (lastRequest === null) return {
1598
- success: true,
1599
- message: null,
1600
- update: {
1601
- lastRequest: now,
1602
- requestCount: 1
1603
- },
1604
- tryAgainIn: null
1615
+ type: "start",
1616
+ now
1605
1617
  };
1606
1618
  const timeSinceLastRequest = now.getTime() - new Date(lastRequest).getTime();
1607
1619
  if (timeSinceLastRequest > rateLimitTimeWindow) return {
1608
- success: true,
1609
- message: null,
1610
- update: {
1611
- lastRequest: now,
1612
- requestCount: 1
1613
- },
1614
- tryAgainIn: null
1620
+ type: "reset",
1621
+ now,
1622
+ windowStart: new Date(now.getTime() - rateLimitTimeWindow)
1615
1623
  };
1616
- if (requestCount >= rateLimitMax) return {
1617
- success: false,
1624
+ if (apiKey.requestCount >= rateLimitMax) return {
1625
+ type: "deny",
1618
1626
  message: API_KEY_ERROR_CODES.RATE_LIMIT_EXCEEDED.message,
1619
- update: null,
1620
1627
  tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest)
1621
1628
  };
1622
- requestCount++;
1623
1629
  return {
1624
- success: true,
1625
- message: null,
1626
- tryAgainIn: null,
1627
- update: {
1628
- lastRequest: now,
1629
- requestCount
1630
- }
1630
+ type: "increment",
1631
+ now,
1632
+ max: rateLimitMax,
1633
+ windowStart: new Date(now.getTime() - rateLimitTimeWindow)
1631
1634
  };
1632
1635
  }
1633
1636
  //#endregion
@@ -1678,8 +1681,6 @@ async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, pe
1678
1681
  if (!apiKeyPermissions) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
1679
1682
  if (!role(apiKeyPermissions).authorize(permissions).success) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
1680
1683
  }
1681
- let remaining = apiKey.remaining;
1682
- let lastRefillAt = apiKey.lastRefillAt;
1683
1684
  if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
1684
1685
  const deleteExhaustedKey = async () => {
1685
1686
  if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
@@ -1705,81 +1706,257 @@ async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, pe
1705
1706
  }));
1706
1707
  else await deleteExhaustedKey();
1707
1708
  throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1708
- } else if (remaining !== null) {
1709
- const now = Date.now();
1710
- const refillInterval = apiKey.refillInterval;
1711
- const refillAmount = apiKey.refillAmount;
1712
- const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
1713
- if (refillInterval && refillAmount) {
1714
- if (now - lastTime > refillInterval) {
1715
- remaining = refillAmount;
1716
- lastRefillAt = /* @__PURE__ */ new Date();
1717
- }
1709
+ }
1710
+ return {
1711
+ apiKey: opts.storage === "database" || opts.storage === "secondary-storage" && opts.fallbackToDatabase ? await claimUsageInDatabase({
1712
+ ctx,
1713
+ apiKey,
1714
+ opts,
1715
+ hashedKey
1716
+ }) : await claimUsageInSecondaryStorage({
1717
+ ctx,
1718
+ apiKey,
1719
+ opts,
1720
+ hashedKey
1721
+ }),
1722
+ opts
1723
+ };
1724
+ }
1725
+ /**
1726
+ * Atomically consume quota and a rate-limit slot against the database row, the
1727
+ * source of truth for `database` and `secondary-storage` + `fallbackToDatabase`
1728
+ * modes. Each guarded `incrementOne` only mutates the row while the guard still
1729
+ * holds, so concurrent verifications cannot drive `remaining` below zero or push
1730
+ * `requestCount` past the configured max. The cache (when present) is refreshed
1731
+ * from the resulting row.
1732
+ */
1733
+ async function claimUsageInDatabase({ ctx, apiKey, opts, hashedKey }) {
1734
+ let row = apiKey;
1735
+ if (apiKey.remaining !== null) row = await consumeRemaining(ctx, apiKey);
1736
+ row = await consumeRateLimit(ctx, row, opts);
1737
+ const finalRow = await ctx.context.adapter.update({
1738
+ model: API_KEY_TABLE_NAME,
1739
+ where: [{
1740
+ field: "id",
1741
+ value: row.id
1742
+ }],
1743
+ update: { updatedAt: /* @__PURE__ */ new Date() }
1744
+ });
1745
+ if (!finalRow) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1746
+ if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) await setApiKey(ctx, finalRow, opts);
1747
+ return finalRow;
1748
+ }
1749
+ /**
1750
+ * Guarded quota consumption. When a refill is due, exactly one verification wins
1751
+ * the refill (compare-and-swap on the observed `lastRefillAt`); any concurrent
1752
+ * verification falls through to the plain guarded decrement against the refilled
1753
+ * value. The decrement only applies while `remaining > 0`, so it can never go
1754
+ * negative. Returns the updated row; throws when the quota is exhausted.
1755
+ */
1756
+ async function consumeRemaining(ctx, apiKey) {
1757
+ const now = /* @__PURE__ */ new Date();
1758
+ const { refillInterval, refillAmount } = apiKey;
1759
+ if (refillInterval && refillAmount) {
1760
+ const lastTime = new Date(apiKey.lastRefillAt ?? apiKey.createdAt).getTime();
1761
+ if (now.getTime() - lastTime > refillInterval) {
1762
+ const refilled = await ctx.context.adapter.incrementOne({
1763
+ model: API_KEY_TABLE_NAME,
1764
+ where: [{
1765
+ field: "id",
1766
+ value: apiKey.id
1767
+ }, {
1768
+ field: "lastRefillAt",
1769
+ value: apiKey.lastRefillAt
1770
+ }],
1771
+ increment: {},
1772
+ set: {
1773
+ remaining: refillAmount - 1,
1774
+ lastRefillAt: now
1775
+ }
1776
+ });
1777
+ if (refilled) return refilled;
1718
1778
  }
1719
- if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1720
- else remaining--;
1721
1779
  }
1722
- const { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);
1723
- if (success === false) throw new APIError$1("TOO_MANY_REQUESTS", {
1724
- message: message ?? void 0,
1780
+ const decremented = await ctx.context.adapter.incrementOne({
1781
+ model: API_KEY_TABLE_NAME,
1782
+ where: [{
1783
+ field: "id",
1784
+ value: apiKey.id
1785
+ }, {
1786
+ field: "remaining",
1787
+ operator: "gt",
1788
+ value: 0
1789
+ }],
1790
+ increment: { remaining: -1 }
1791
+ });
1792
+ if (!decremented) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1793
+ return decremented;
1794
+ }
1795
+ /**
1796
+ * Guarded rate-limit consumption. The common in-window path increments
1797
+ * `requestCount` only while it is below the max (compare-and-swap), so a burst
1798
+ * of concurrent verifications can never exceed the limit. Window resets and the
1799
+ * first request in a window are guarded conditional sets; a request that loses
1800
+ * every guard within an active window is rejected. Returns the updated row, or
1801
+ * the unchanged row when rate limiting does not apply.
1802
+ */
1803
+ async function consumeRateLimit(ctx, apiKey, opts) {
1804
+ const decision = evaluateRateLimit(apiKey, opts);
1805
+ if (decision.type === "deny") throw new APIError$1("TOO_MANY_REQUESTS", {
1806
+ message: decision.message,
1725
1807
  code: "RATE_LIMITED",
1726
- details: { tryAgainIn }
1808
+ details: { tryAgainIn: decision.tryAgainIn }
1727
1809
  });
1728
- const mutations = {
1729
- ...update,
1730
- remaining,
1731
- lastRefillAt,
1732
- updatedAt: /* @__PURE__ */ new Date()
1733
- };
1734
- const updated = {
1735
- ...apiKey,
1736
- ...mutations
1737
- };
1738
- const performUpdate = async () => {
1739
- if (opts.storage === "database") return ctx.context.adapter.update({
1740
- model: API_KEY_TABLE_NAME,
1810
+ if (decision.type === "skip") {
1811
+ if (decision.lastRequest === null) return apiKey;
1812
+ return await ctx.context.adapter.update({
1813
+ model: "apikey",
1741
1814
  where: [{
1742
1815
  field: "id",
1743
1816
  value: apiKey.id
1744
1817
  }],
1745
- update: mutations
1746
- });
1747
- else if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
1748
- const dbUpdated = await ctx.context.adapter.update({
1749
- model: API_KEY_TABLE_NAME,
1750
- where: [{
1818
+ update: { lastRequest: decision.lastRequest }
1819
+ }) ?? apiKey;
1820
+ }
1821
+ if (decision.type === "increment") {
1822
+ const incremented = await ctx.context.adapter.incrementOne({
1823
+ model: API_KEY_TABLE_NAME,
1824
+ where: [
1825
+ {
1751
1826
  field: "id",
1752
1827
  value: apiKey.id
1753
- }],
1754
- update: mutations
1755
- });
1756
- if (dbUpdated) await setApiKey(ctx, dbUpdated, opts);
1757
- return dbUpdated;
1758
- } else {
1759
- const fresh = await getApiKey$1(ctx, hashedKey, opts);
1760
- if (!fresh) return null;
1761
- const merged = {
1762
- ...fresh,
1763
- ...mutations
1764
- };
1765
- await setApiKey(ctx, merged, opts);
1766
- return merged;
1828
+ },
1829
+ {
1830
+ field: "lastRequest",
1831
+ operator: "gt",
1832
+ value: decision.windowStart
1833
+ },
1834
+ {
1835
+ field: "requestCount",
1836
+ operator: "lt",
1837
+ value: decision.max
1838
+ }
1839
+ ],
1840
+ increment: { requestCount: 1 },
1841
+ set: { lastRequest: decision.now }
1842
+ });
1843
+ if (incremented) return incremented;
1844
+ const fresh = await ctx.context.adapter.findOne({
1845
+ model: API_KEY_TABLE_NAME,
1846
+ where: [{
1847
+ field: "id",
1848
+ value: apiKey.id
1849
+ }]
1850
+ });
1851
+ if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1852
+ return consumeRateLimit(ctx, fresh, opts);
1853
+ }
1854
+ const windowGuard = decision.type === "reset" ? {
1855
+ field: "lastRequest",
1856
+ operator: "lte",
1857
+ value: decision.windowStart
1858
+ } : {
1859
+ field: "lastRequest",
1860
+ operator: "eq",
1861
+ value: null
1862
+ };
1863
+ const started = await ctx.context.adapter.incrementOne({
1864
+ model: API_KEY_TABLE_NAME,
1865
+ where: [{
1866
+ field: "id",
1867
+ value: apiKey.id
1868
+ }, windowGuard],
1869
+ increment: {},
1870
+ set: {
1871
+ requestCount: 1,
1872
+ lastRequest: decision.now
1767
1873
  }
1874
+ });
1875
+ if (started) return started;
1876
+ const fresh = await ctx.context.adapter.findOne({
1877
+ model: API_KEY_TABLE_NAME,
1878
+ where: [{
1879
+ field: "id",
1880
+ value: apiKey.id
1881
+ }]
1882
+ });
1883
+ if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1884
+ return consumeRateLimit(ctx, fresh, opts);
1885
+ }
1886
+ /**
1887
+ * Secondary-storage-only mode has no database row to guard, so quota and
1888
+ * rate-limit consumption stays a read-modify-write merge over the serialized
1889
+ * key. This is the residual non-atomic path; strict enforcement requires the
1890
+ * database (use `fallbackToDatabase`) or an atomic secondary-storage primitive.
1891
+ * FIXME(api-key-secondary-atomic): back this with SecondaryStorage.increment on
1892
+ * `next` so secondary-storage-only mode enforces quota and rate limits atomically.
1893
+ */
1894
+ async function claimUsageInSecondaryStorage({ ctx, apiKey, opts, hashedKey }) {
1895
+ let remaining = apiKey.remaining;
1896
+ let lastRefillAt = apiKey.lastRefillAt;
1897
+ if (remaining !== null) {
1898
+ const now = Date.now();
1899
+ const { refillInterval, refillAmount } = apiKey;
1900
+ const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
1901
+ if (refillInterval && refillAmount && now - lastTime > refillInterval) {
1902
+ remaining = refillAmount;
1903
+ lastRefillAt = /* @__PURE__ */ new Date();
1904
+ }
1905
+ if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1906
+ remaining--;
1907
+ }
1908
+ const mutations = {
1909
+ ...applyRateLimitToSnapshot(apiKey, opts),
1910
+ remaining,
1911
+ lastRefillAt,
1912
+ updatedAt: /* @__PURE__ */ new Date()
1913
+ };
1914
+ const performUpdate = async () => {
1915
+ const fresh = await getApiKey$1(ctx, hashedKey, opts);
1916
+ if (!fresh) return null;
1917
+ const merged = {
1918
+ ...fresh,
1919
+ ...mutations
1920
+ };
1921
+ await setApiKey(ctx, merged, opts);
1922
+ return merged;
1768
1923
  };
1769
- let newApiKey = null;
1770
1924
  if (opts.deferUpdates) {
1771
1925
  ctx.context.runInBackground(performUpdate().catch((error) => {
1772
1926
  ctx.context.logger.error("Failed to update API key:", error);
1773
1927
  }));
1774
- newApiKey = updated;
1775
- } else {
1776
- newApiKey = await performUpdate();
1777
- if (!newApiKey) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
1928
+ return {
1929
+ ...apiKey,
1930
+ ...mutations
1931
+ };
1932
+ }
1933
+ const updated = await performUpdate();
1934
+ if (!updated) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
1935
+ return updated;
1936
+ }
1937
+ /**
1938
+ * Translate a rate-limit decision into a counter snapshot for the
1939
+ * secondary-storage merge write. Denials throw before any write.
1940
+ */
1941
+ function applyRateLimitToSnapshot(apiKey, opts) {
1942
+ const decision = evaluateRateLimit(apiKey, opts);
1943
+ switch (decision.type) {
1944
+ case "deny": throw new APIError$1("TOO_MANY_REQUESTS", {
1945
+ message: decision.message,
1946
+ code: "RATE_LIMITED",
1947
+ details: { tryAgainIn: decision.tryAgainIn }
1948
+ });
1949
+ case "skip": return decision.lastRequest === null ? {} : { lastRequest: decision.lastRequest };
1950
+ case "start":
1951
+ case "reset": return {
1952
+ lastRequest: decision.now,
1953
+ requestCount: 1
1954
+ };
1955
+ case "increment": return {
1956
+ lastRequest: decision.now,
1957
+ requestCount: apiKey.requestCount + 1
1958
+ };
1778
1959
  }
1779
- return {
1780
- apiKey: newApiKey,
1781
- opts
1782
- };
1783
1960
  }
1784
1961
  const verifyApiKeyBodySchema = z.object({
1785
1962
  configId: z.string().meta({ description: "Configuration ID to scope verification to. When omitted, the key is validated against its own configuration." }).optional(),
@@ -1787,7 +1964,7 @@ const verifyApiKeyBodySchema = z.object({
1787
1964
  permissions: z.record(z.string(), z.array(z.string())).meta({ description: "The permissions to verify." }).optional()
1788
1965
  });
1789
1966
  function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1790
- return createAuthEndpoint({
1967
+ return createAuthEndpoint.serverOnly({
1791
1968
  method: "POST",
1792
1969
  body: verifyApiKeyBodySchema
1793
1970
  }, async (ctx) => {
@@ -1,6 +1,7 @@
1
1
  import * as better_auth0 from "better-auth";
2
2
  import { Statements } from "better-auth/plugins/access";
3
3
  import { Awaitable, GenericEndpointContext, HookEndpointContext, LiteralString } from "@better-auth/core";
4
+ import { SecondaryStorage } from "@better-auth/core/db";
4
5
  import { InferOptionSchema } from "better-auth/types";
5
6
 
6
7
  //#region src/schema.d.ts
@@ -417,20 +418,7 @@ interface ApiKeyConfigurationOptions {
417
418
  * Useful when you want to use a different storage backend specifically for API keys,
418
419
  * or when you need custom logic for storage operations.
419
420
  */
420
- customStorage?: {
421
- /**
422
- * Get a value from storage
423
- */
424
- get: (key: string) => Awaitable<unknown>;
425
- /**
426
- * Set a value in storage
427
- */
428
- set: (key: string, value: string, ttl?: number | undefined) => Awaitable<void | null | unknown>;
429
- /**
430
- * Delete a value from storage
431
- */
432
- delete: (key: string) => Awaitable<void | null | string>;
433
- } | undefined;
421
+ customStorage?: SecondaryStorage | undefined;
434
422
  /**
435
423
  * Defer non-critical updates (rate limiting counters, timestamps, remaining count)
436
424
  * to run after the response is sent using the global `advanced.backgroundTasks` handler.
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-BR70O3Q3.mjs";
1
+ import { n as ApiKeyConfigurationOptions, r as ApiKeyOptions, t as ApiKey } from "./types-CbtANSbR.mjs";
2
2
  export { ApiKey, ApiKeyConfigurationOptions, ApiKeyOptions };
@@ -35,6 +35,6 @@ const API_KEY_ERROR_CODES = defineErrorCodes({
35
35
  });
36
36
  //#endregion
37
37
  //#region src/version.ts
38
- const PACKAGE_VERSION = "1.7.0-beta.5";
38
+ const PACKAGE_VERSION = "1.7.0-beta.7";
39
39
  //#endregion
40
40
  export { API_KEY_ERROR_CODES as n, PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/api-key",
3
- "version": "1.7.0-beta.5",
3
+ "version": "1.7.0-beta.7",
4
4
  "description": "API Key plugin for Better Auth.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,14 +61,14 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "tsdown": "0.21.1",
64
- "@better-auth/core": "1.7.0-beta.5",
65
- "better-auth": "1.7.0-beta.5"
64
+ "@better-auth/core": "1.7.0-beta.7",
65
+ "better-auth": "1.7.0-beta.7"
66
66
  },
67
67
  "peerDependencies": {
68
- "@better-auth/utils": "0.4.1",
68
+ "@better-auth/utils": "0.4.2",
69
69
  "better-call": "1.3.6",
70
- "@better-auth/core": "^1.7.0-beta.5",
71
- "better-auth": "^1.7.0-beta.5"
70
+ "@better-auth/core": "^1.7.0-beta.7",
71
+ "better-auth": "^1.7.0-beta.7"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsdown",