@better-auth/api-key 1.6.15 → 1.6.17

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-brpjhtFx.mjs";
1
+ import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-ZL7h_G4d.mjs";
2
2
  //#region src/client.ts
3
3
  const apiKeyClient = () => {
4
4
  return {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-brpjhtFx.mjs";
1
+ import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-ZL7h_G4d.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,47 @@ 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 run = (refListLocks.get(refKey) ?? Promise.resolve()).then(task, task);
187
+ const tracked = run.finally(() => {
188
+ if (refListLocks.get(refKey) === tracked) refListLocks.delete(refKey);
189
+ });
190
+ refListLocks.set(refKey, tracked);
191
+ return run;
192
+ }
193
+ /**
170
194
  * Read-modify-write the ref list:
171
195
  * used only when the list is the source of truth.
172
196
  */
173
197
  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));
198
+ await withRefListLock(refKey, async () => {
199
+ const refListData = await storage.get(refKey);
200
+ let keyIds = [];
201
+ if (refListData && typeof refListData === "string") try {
202
+ keyIds = JSON.parse(refListData);
203
+ } catch {
204
+ keyIds = [];
205
+ }
206
+ else if (Array.isArray(refListData)) keyIds = refListData;
207
+ const next = modify(keyIds);
208
+ if (next.length === 0) await storage.delete(refKey);
209
+ else await storage.set(refKey, JSON.stringify(next));
210
+ });
185
211
  }
186
212
  async function setApiKeyInStorage(_ctx, apiKey, storage, ttl, opts) {
187
213
  const serialized = serializeApiKey(apiKey);
@@ -720,7 +746,7 @@ function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllEx
720
746
  const { configId, name, expiresIn, prefix, remaining, metadata, refillAmount, refillInterval, permissions, rateLimitMax, rateLimitTimeWindow, rateLimitEnabled } = ctx.body;
721
747
  const opts = resolveConfiguration(ctx.context, configurations, configId);
722
748
  const keyGenerator = opts.customKeyGenerator || defaultKeyGenerator;
723
- const session = await getSessionFromCtx(ctx);
749
+ const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
724
750
  const isClientRequest = ctx.request || ctx.headers;
725
751
  if (isClientRequest && (refillAmount !== void 0 || refillInterval !== void 0 || rateLimitMax !== void 0 || rateLimitTimeWindow !== void 0 || rateLimitEnabled !== void 0 || permissions !== void 0 || remaining !== null)) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.SERVER_ONLY_PROPERTY);
726
752
  if (ctx.request && ctx.body.userId !== void 0) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
@@ -852,7 +878,7 @@ function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllEx
852
878
  //#endregion
853
879
  //#region src/routes/delete-all-expired-api-keys.ts
854
880
  function deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys }) {
855
- return createAuthEndpoint({ method: "POST" }, async (ctx) => {
881
+ return createAuthEndpoint.serverOnly({ method: "POST" }, async (ctx) => {
856
882
  try {
857
883
  await deleteAllExpiredApiKeys(ctx.context, true);
858
884
  } catch (error) {
@@ -1467,7 +1493,7 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1467
1493
  } }
1468
1494
  }, async (ctx) => {
1469
1495
  const { configId, keyId, expiresIn, enabled, metadata, refillAmount, refillInterval, remaining, name, permissions, rateLimitEnabled, rateLimitTimeWindow, rateLimitMax } = ctx.body;
1470
- const session = await getSessionFromCtx(ctx);
1496
+ const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
1471
1497
  const authRequired = ctx.request || ctx.headers;
1472
1498
  const user = authRequired && !session ? null : session?.user || { id: ctx.body.userId };
1473
1499
  if (!user?.id) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
@@ -1565,75 +1591,54 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1565
1591
  //#endregion
1566
1592
  //#region src/rate-limit.ts
1567
1593
  /**
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.
1594
+ * Decides how the current request affects the per-key rate-limit counter, based
1595
+ * on the read-in-memory ApiKey. The verify route applies the result atomically;
1596
+ * this function performs no writes.
1572
1597
  */
1573
- function isRateLimited(apiKey, opts) {
1598
+ function evaluateRateLimit(apiKey, opts) {
1574
1599
  const now = /* @__PURE__ */ new Date();
1575
1600
  const lastRequest = apiKey.lastRequest;
1576
1601
  const rateLimitTimeWindow = apiKey.rateLimitTimeWindow;
1577
1602
  const rateLimitMax = apiKey.rateLimitMax;
1578
- let requestCount = apiKey.requestCount;
1579
1603
  if (opts.rateLimit.enabled === false) return {
1580
- success: true,
1581
- message: null,
1582
- update: { lastRequest: now },
1583
- tryAgainIn: null
1604
+ type: "skip",
1605
+ lastRequest: now
1584
1606
  };
1585
1607
  if (apiKey.rateLimitEnabled === false) return {
1586
- success: true,
1587
- message: null,
1588
- update: { lastRequest: now },
1589
- tryAgainIn: null
1608
+ type: "skip",
1609
+ lastRequest: now
1590
1610
  };
1591
1611
  if (rateLimitTimeWindow === null || rateLimitMax === null) return {
1592
- success: true,
1593
- message: null,
1594
- update: null,
1595
- tryAgainIn: null
1612
+ type: "skip",
1613
+ lastRequest: null
1596
1614
  };
1597
1615
  if (lastRequest === null) return {
1598
- success: true,
1599
- message: null,
1600
- update: {
1601
- lastRequest: now,
1602
- requestCount: 1
1603
- },
1604
- tryAgainIn: null
1616
+ type: "start",
1617
+ now
1605
1618
  };
1606
1619
  const timeSinceLastRequest = now.getTime() - new Date(lastRequest).getTime();
1607
1620
  if (timeSinceLastRequest > rateLimitTimeWindow) return {
1608
- success: true,
1609
- message: null,
1610
- update: {
1611
- lastRequest: now,
1612
- requestCount: 1
1613
- },
1614
- tryAgainIn: null
1621
+ type: "reset",
1622
+ now,
1623
+ windowStart: new Date(now.getTime() - rateLimitTimeWindow)
1615
1624
  };
1616
- if (requestCount >= rateLimitMax) return {
1617
- success: false,
1625
+ if (apiKey.requestCount >= rateLimitMax) return {
1626
+ type: "deny",
1618
1627
  message: API_KEY_ERROR_CODES.RATE_LIMIT_EXCEEDED.message,
1619
- update: null,
1620
1628
  tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest)
1621
1629
  };
1622
- requestCount++;
1623
1630
  return {
1624
- success: true,
1625
- message: null,
1626
- tryAgainIn: null,
1627
- update: {
1628
- lastRequest: now,
1629
- requestCount
1630
- }
1631
+ type: "increment",
1632
+ now,
1633
+ max: rateLimitMax,
1634
+ windowStart: new Date(now.getTime() - rateLimitTimeWindow)
1631
1635
  };
1632
1636
  }
1633
1637
  //#endregion
1634
1638
  //#region src/routes/verify-api-key.ts
1635
1639
  async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, permissions, expectedConfigId, runCustomValidator }) {
1636
- const apiKey = await getApiKey$1(ctx, lookupOpts.disableKeyHashing ? key : await defaultKeyHasher(key), lookupOpts);
1640
+ const hashedKey = lookupOpts.disableKeyHashing ? key : await defaultKeyHasher(key);
1641
+ const apiKey = await getApiKey$1(ctx, hashedKey, lookupOpts);
1637
1642
  if (!apiKey) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1638
1643
  if (expectedConfigId !== void 0 && !configIdMatches(apiKey.configId, expectedConfigId)) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1639
1644
  const opts = resolveConfiguration(ctx.context, configurations, apiKey.configId);
@@ -1677,8 +1682,6 @@ async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, pe
1677
1682
  if (!apiKeyPermissions) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
1678
1683
  if (!role(apiKeyPermissions).authorize(permissions).success) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
1679
1684
  }
1680
- let remaining = apiKey.remaining;
1681
- let lastRefillAt = apiKey.lastRefillAt;
1682
1685
  if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
1683
1686
  const deleteExhaustedKey = async () => {
1684
1687
  if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
@@ -1704,78 +1707,257 @@ async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, pe
1704
1707
  }));
1705
1708
  else await deleteExhaustedKey();
1706
1709
  throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1707
- } else if (remaining !== null) {
1708
- const now = Date.now();
1709
- const refillInterval = apiKey.refillInterval;
1710
- const refillAmount = apiKey.refillAmount;
1711
- const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
1712
- if (refillInterval && refillAmount) {
1713
- if (now - lastTime > refillInterval) {
1714
- remaining = refillAmount;
1715
- lastRefillAt = /* @__PURE__ */ new Date();
1716
- }
1710
+ }
1711
+ return {
1712
+ apiKey: opts.storage === "database" || opts.storage === "secondary-storage" && opts.fallbackToDatabase ? await claimUsageInDatabase({
1713
+ ctx,
1714
+ apiKey,
1715
+ opts,
1716
+ hashedKey
1717
+ }) : await claimUsageInSecondaryStorage({
1718
+ ctx,
1719
+ apiKey,
1720
+ opts,
1721
+ hashedKey
1722
+ }),
1723
+ opts
1724
+ };
1725
+ }
1726
+ /**
1727
+ * Atomically consume quota and a rate-limit slot against the database row, the
1728
+ * source of truth for `database` and `secondary-storage` + `fallbackToDatabase`
1729
+ * modes. Each guarded `incrementOne` only mutates the row while the guard still
1730
+ * holds, so concurrent verifications cannot drive `remaining` below zero or push
1731
+ * `requestCount` past the configured max. The cache (when present) is refreshed
1732
+ * from the resulting row.
1733
+ */
1734
+ async function claimUsageInDatabase({ ctx, apiKey, opts, hashedKey }) {
1735
+ let row = apiKey;
1736
+ if (apiKey.remaining !== null) row = await consumeRemaining(ctx, apiKey);
1737
+ row = await consumeRateLimit(ctx, row, opts);
1738
+ const finalRow = await ctx.context.adapter.update({
1739
+ model: API_KEY_TABLE_NAME,
1740
+ where: [{
1741
+ field: "id",
1742
+ value: row.id
1743
+ }],
1744
+ update: { updatedAt: /* @__PURE__ */ new Date() }
1745
+ });
1746
+ if (!finalRow) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1747
+ if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) await setApiKey(ctx, finalRow, opts);
1748
+ return finalRow;
1749
+ }
1750
+ /**
1751
+ * Guarded quota consumption. When a refill is due, exactly one verification wins
1752
+ * the refill (compare-and-swap on the observed `lastRefillAt`); any concurrent
1753
+ * verification falls through to the plain guarded decrement against the refilled
1754
+ * value. The decrement only applies while `remaining > 0`, so it can never go
1755
+ * negative. Returns the updated row; throws when the quota is exhausted.
1756
+ */
1757
+ async function consumeRemaining(ctx, apiKey) {
1758
+ const now = /* @__PURE__ */ new Date();
1759
+ const { refillInterval, refillAmount } = apiKey;
1760
+ if (refillInterval && refillAmount) {
1761
+ const lastTime = new Date(apiKey.lastRefillAt ?? apiKey.createdAt).getTime();
1762
+ if (now.getTime() - lastTime > refillInterval) {
1763
+ const refilled = await ctx.context.adapter.incrementOne({
1764
+ model: API_KEY_TABLE_NAME,
1765
+ where: [{
1766
+ field: "id",
1767
+ value: apiKey.id
1768
+ }, {
1769
+ field: "lastRefillAt",
1770
+ value: apiKey.lastRefillAt
1771
+ }],
1772
+ increment: {},
1773
+ set: {
1774
+ remaining: refillAmount - 1,
1775
+ lastRefillAt: now
1776
+ }
1777
+ });
1778
+ if (refilled) return refilled;
1717
1779
  }
1718
- if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1719
- else remaining--;
1720
1780
  }
1721
- const { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);
1722
- if (success === false) throw new APIError$1("TOO_MANY_REQUESTS", {
1723
- message: message ?? void 0,
1781
+ const decremented = await ctx.context.adapter.incrementOne({
1782
+ model: API_KEY_TABLE_NAME,
1783
+ where: [{
1784
+ field: "id",
1785
+ value: apiKey.id
1786
+ }, {
1787
+ field: "remaining",
1788
+ operator: "gt",
1789
+ value: 0
1790
+ }],
1791
+ increment: { remaining: -1 }
1792
+ });
1793
+ if (!decremented) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1794
+ return decremented;
1795
+ }
1796
+ /**
1797
+ * Guarded rate-limit consumption. The common in-window path increments
1798
+ * `requestCount` only while it is below the max (compare-and-swap), so a burst
1799
+ * of concurrent verifications can never exceed the limit. Window resets and the
1800
+ * first request in a window are guarded conditional sets; a request that loses
1801
+ * every guard within an active window is rejected. Returns the updated row, or
1802
+ * the unchanged row when rate limiting does not apply.
1803
+ */
1804
+ async function consumeRateLimit(ctx, apiKey, opts) {
1805
+ const decision = evaluateRateLimit(apiKey, opts);
1806
+ if (decision.type === "deny") throw new APIError$1("TOO_MANY_REQUESTS", {
1807
+ message: decision.message,
1724
1808
  code: "RATE_LIMITED",
1725
- details: { tryAgainIn }
1809
+ details: { tryAgainIn: decision.tryAgainIn }
1726
1810
  });
1727
- const updated = {
1728
- ...apiKey,
1729
- ...update,
1730
- remaining,
1731
- lastRefillAt,
1732
- updatedAt: /* @__PURE__ */ new Date()
1733
- };
1734
- const performUpdate = async () => {
1735
- if (opts.storage === "database") return ctx.context.adapter.update({
1736
- model: API_KEY_TABLE_NAME,
1811
+ if (decision.type === "skip") {
1812
+ if (decision.lastRequest === null) return apiKey;
1813
+ return await ctx.context.adapter.update({
1814
+ model: "apikey",
1737
1815
  where: [{
1738
1816
  field: "id",
1739
1817
  value: apiKey.id
1740
1818
  }],
1741
- update: {
1742
- ...updated,
1743
- id: void 0
1744
- }
1745
- });
1746
- else if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
1747
- const dbUpdated = await ctx.context.adapter.update({
1748
- model: API_KEY_TABLE_NAME,
1749
- where: [{
1819
+ update: { lastRequest: decision.lastRequest }
1820
+ }) ?? apiKey;
1821
+ }
1822
+ if (decision.type === "increment") {
1823
+ const incremented = await ctx.context.adapter.incrementOne({
1824
+ model: API_KEY_TABLE_NAME,
1825
+ where: [
1826
+ {
1750
1827
  field: "id",
1751
1828
  value: apiKey.id
1752
- }],
1753
- update: {
1754
- ...updated,
1755
- id: void 0
1829
+ },
1830
+ {
1831
+ field: "lastRequest",
1832
+ operator: "gt",
1833
+ value: decision.windowStart
1834
+ },
1835
+ {
1836
+ field: "requestCount",
1837
+ operator: "lt",
1838
+ value: decision.max
1756
1839
  }
1757
- });
1758
- if (dbUpdated) await setApiKey(ctx, dbUpdated, opts);
1759
- return dbUpdated;
1760
- } else {
1761
- await setApiKey(ctx, updated, opts);
1762
- return updated;
1840
+ ],
1841
+ increment: { requestCount: 1 },
1842
+ set: { lastRequest: decision.now }
1843
+ });
1844
+ if (incremented) return incremented;
1845
+ const fresh = await ctx.context.adapter.findOne({
1846
+ model: API_KEY_TABLE_NAME,
1847
+ where: [{
1848
+ field: "id",
1849
+ value: apiKey.id
1850
+ }]
1851
+ });
1852
+ if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1853
+ return consumeRateLimit(ctx, fresh, opts);
1854
+ }
1855
+ const windowGuard = decision.type === "reset" ? {
1856
+ field: "lastRequest",
1857
+ operator: "lte",
1858
+ value: decision.windowStart
1859
+ } : {
1860
+ field: "lastRequest",
1861
+ operator: "eq",
1862
+ value: null
1863
+ };
1864
+ const started = await ctx.context.adapter.incrementOne({
1865
+ model: API_KEY_TABLE_NAME,
1866
+ where: [{
1867
+ field: "id",
1868
+ value: apiKey.id
1869
+ }, windowGuard],
1870
+ increment: {},
1871
+ set: {
1872
+ requestCount: 1,
1873
+ lastRequest: decision.now
1874
+ }
1875
+ });
1876
+ if (started) return started;
1877
+ const fresh = await ctx.context.adapter.findOne({
1878
+ model: API_KEY_TABLE_NAME,
1879
+ where: [{
1880
+ field: "id",
1881
+ value: apiKey.id
1882
+ }]
1883
+ });
1884
+ if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
1885
+ return consumeRateLimit(ctx, fresh, opts);
1886
+ }
1887
+ /**
1888
+ * Secondary-storage-only mode has no database row to guard, so quota and
1889
+ * rate-limit consumption stays a read-modify-write merge over the serialized
1890
+ * key. This is the residual non-atomic path; strict enforcement requires the
1891
+ * database (use `fallbackToDatabase`) or an atomic secondary-storage primitive.
1892
+ * FIXME(api-key-secondary-atomic): back this with SecondaryStorage.increment on
1893
+ * `next` so secondary-storage-only mode enforces quota and rate limits atomically.
1894
+ */
1895
+ async function claimUsageInSecondaryStorage({ ctx, apiKey, opts, hashedKey }) {
1896
+ let remaining = apiKey.remaining;
1897
+ let lastRefillAt = apiKey.lastRefillAt;
1898
+ if (remaining !== null) {
1899
+ const now = Date.now();
1900
+ const { refillInterval, refillAmount } = apiKey;
1901
+ const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
1902
+ if (refillInterval && refillAmount && now - lastTime > refillInterval) {
1903
+ remaining = refillAmount;
1904
+ lastRefillAt = /* @__PURE__ */ new Date();
1763
1905
  }
1906
+ if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
1907
+ remaining--;
1908
+ }
1909
+ const mutations = {
1910
+ ...applyRateLimitToSnapshot(apiKey, opts),
1911
+ remaining,
1912
+ lastRefillAt,
1913
+ updatedAt: /* @__PURE__ */ new Date()
1914
+ };
1915
+ const performUpdate = async () => {
1916
+ const fresh = await getApiKey$1(ctx, hashedKey, opts);
1917
+ if (!fresh) return null;
1918
+ const merged = {
1919
+ ...fresh,
1920
+ ...mutations
1921
+ };
1922
+ await setApiKey(ctx, merged, opts);
1923
+ return merged;
1764
1924
  };
1765
- let newApiKey = null;
1766
1925
  if (opts.deferUpdates) {
1767
1926
  ctx.context.runInBackground(performUpdate().catch((error) => {
1768
1927
  ctx.context.logger.error("Failed to update API key:", error);
1769
1928
  }));
1770
- newApiKey = updated;
1771
- } else {
1772
- newApiKey = await performUpdate();
1773
- if (!newApiKey) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
1929
+ return {
1930
+ ...apiKey,
1931
+ ...mutations
1932
+ };
1933
+ }
1934
+ const updated = await performUpdate();
1935
+ if (!updated) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
1936
+ return updated;
1937
+ }
1938
+ /**
1939
+ * Translate a rate-limit decision into a counter snapshot for the
1940
+ * secondary-storage merge write. Denials throw before any write.
1941
+ */
1942
+ function applyRateLimitToSnapshot(apiKey, opts) {
1943
+ const decision = evaluateRateLimit(apiKey, opts);
1944
+ switch (decision.type) {
1945
+ case "deny": throw new APIError$1("TOO_MANY_REQUESTS", {
1946
+ message: decision.message,
1947
+ code: "RATE_LIMITED",
1948
+ details: { tryAgainIn: decision.tryAgainIn }
1949
+ });
1950
+ case "skip": return decision.lastRequest === null ? {} : { lastRequest: decision.lastRequest };
1951
+ case "start":
1952
+ case "reset": return {
1953
+ lastRequest: decision.now,
1954
+ requestCount: 1
1955
+ };
1956
+ case "increment": return {
1957
+ lastRequest: decision.now,
1958
+ requestCount: apiKey.requestCount + 1
1959
+ };
1774
1960
  }
1775
- return {
1776
- apiKey: newApiKey,
1777
- opts
1778
- };
1779
1961
  }
1780
1962
  const verifyApiKeyBodySchema = z.object({
1781
1963
  configId: z.string().meta({ description: "Configuration ID to scope verification to. When omitted, the key is validated against its own configuration." }).optional(),
@@ -1783,7 +1965,7 @@ const verifyApiKeyBodySchema = z.object({
1783
1965
  permissions: z.record(z.string(), z.array(z.string())).meta({ description: "The permissions to verify." }).optional()
1784
1966
  });
1785
1967
  function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
1786
- return createAuthEndpoint({
1968
+ return createAuthEndpoint.serverOnly({
1787
1969
  method: "POST",
1788
1970
  body: verifyApiKeyBodySchema
1789
1971
  }, async (ctx) => {
@@ -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.6.15";
38
+ const PACKAGE_VERSION = "1.6.17";
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.6.15",
3
+ "version": "1.6.17",
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.6.15",
65
- "better-auth": "1.6.15"
64
+ "@better-auth/core": "1.6.17",
65
+ "better-auth": "1.6.17"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@better-auth/utils": "0.4.1",
69
- "better-call": "1.3.5",
70
- "@better-auth/core": "^1.6.15",
71
- "better-auth": "^1.6.15"
69
+ "better-call": "1.3.6",
70
+ "@better-auth/core": "^1.6.17",
71
+ "better-auth": "^1.6.17"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsdown",