diff --git a/apps/agent/integration/identities/identities_ratelimits_accuracy_test.go b/apps/agent/integration/identities/identities_ratelimits_accuracy_test.go index de5f8e0016..bad2d7ec14 100644 --- a/apps/agent/integration/identities/identities_ratelimits_accuracy_test.go +++ b/apps/agent/integration/identities/identities_ratelimits_accuracy_test.go @@ -32,7 +32,11 @@ func TestIdentitiesRatelimitAccuracy(t *testing.T) { unkey.WithSecurity(rootKey), ) +<<<<<<< HEAD for _, nKeys := range []int{1, 3, 10, 1000} { +======= + for _, nKeys := range []int{1} { //, 3, 10, 1000} { +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) t.Run(fmt.Sprintf("with %d keys", nKeys), func(t *testing.T) { for _, tc := range []struct { @@ -133,7 +137,11 @@ func TestIdentitiesRatelimitAccuracy(t *testing.T) { // --------------------------------------------------------------------------- exactLimit := int(inferenceLimit.Limit) * int(tc.testDuration/(time.Duration(inferenceLimit.Duration)*time.Millisecond)) +<<<<<<< HEAD upperLimit := int(2.5 * float64(exactLimit)) +======= + upperLimit := int(1.2 * float64(exactLimit)) +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) lowerLimit := exactLimit if total < lowerLimit { lowerLimit = total diff --git a/apps/agent/integration/keys/ratelimits_test.go b/apps/agent/integration/keys/ratelimits_test.go index efac5eed68..69158dfe23 100644 --- a/apps/agent/integration/keys/ratelimits_test.go +++ b/apps/agent/integration/keys/ratelimits_test.go @@ -109,7 +109,11 @@ func TestDefaultRatelimitAccuracy(t *testing.T) { // --------------------------------------------------------------------------- exactLimit := int(ratelimit.Limit) * int(tc.testDuration/(time.Duration(*ratelimit.Duration)*time.Millisecond)) +<<<<<<< HEAD upperLimit := int(2.5 * float64(exactLimit)) +======= + upperLimit := int(1.2 * float64(exactLimit)) +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) lowerLimit := exactLimit if total < lowerLimit { lowerLimit = total diff --git a/apps/api/src/pkg/middleware/metrics.ts b/apps/api/src/pkg/middleware/metrics.ts index 87be7f43c4..8aaa318318 100644 --- a/apps/api/src/pkg/middleware/metrics.ts +++ b/apps/api/src/pkg/middleware/metrics.ts @@ -8,8 +8,11 @@ export function metrics(): MiddlewareHandler { return async (c, next) => { const { metrics, analytics, logger } = c.get("services"); +<<<<<<< HEAD let requestBody = await c.req.raw.clone().text(); requestBody = requestBody.replaceAll(/"key":\s*"[a-zA-Z0-9_]+"/g, '"key": ""'); +======= +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) const start = performance.now(); const m = { isolateId: c.get("isolateId"), diff --git a/apps/api/src/pkg/ratelimit/client.ts b/apps/api/src/pkg/ratelimit/client.ts index fc231d9d04..531ff18852 100644 --- a/apps/api/src/pkg/ratelimit/client.ts +++ b/apps/api/src/pkg/ratelimit/client.ts @@ -15,13 +15,13 @@ import { export class AgentRatelimiter implements RateLimiter { private readonly logger: Logger; private readonly metrics: Metrics; - private readonly cache: Map; + private readonly cache: Map; private readonly agent: Agent; constructor(opts: { agent: { url: string; token: string }; logger: Logger; metrics: Metrics; - cache: Map; + cache: Map; }) { this.logger = opts.logger; this.metrics = opts.metrics; @@ -36,7 +36,11 @@ export class AgentRatelimiter implements RateLimiter { return [req.identifier, window, req.shard].join("::"); } +<<<<<<< HEAD private setCacheMax(id: string, current: number, reset: number) { +======= + private setCache(id: string, current: number, reset: number, blocked: boolean) { +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) const maxEntries = 10_000; this.metrics.emit({ metric: "metric.cache.size", @@ -55,11 +59,15 @@ export class AgentRatelimiter implements RateLimiter { } } } +<<<<<<< HEAD const cached = this.cache.get(id) ?? { reset: 0, current: 0 }; if (current > cached.current) { this.cache.set(id, { reset, current }); return current; } +======= + this.cache.set(id, { reset, current, blocked }); +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) } public async limit( @@ -127,8 +135,8 @@ export class AgentRatelimiter implements RateLimiter { * This might not happen too often, but in extreme cases the cache should hit and we can skip * the request to the durable object entirely, which speeds everything up and is cheaper for us */ - const cached = this.cache.get(id) ?? { current: 0, reset: 0 }; - if (cached.current >= req.limit) { + const cached = this.cache.get(id) ?? { current: 0, reset: 0, blocked: false }; + if (cached.blocked) { return Ok({ pass: false, current: cached.current, @@ -165,7 +173,7 @@ export class AgentRatelimiter implements RateLimiter { if (sync) { const res = await p; if (res.val) { - this.setCacheMax(id, res.val.current, res.val.reset); + this.setCache(id, res.val.current, res.val.reset, !res.val.pass); } return res; } @@ -176,7 +184,7 @@ export class AgentRatelimiter implements RateLimiter { this.logger.error(res.err.message); return; } - this.setCacheMax(id, res.val.current, res.val.reset); + this.setCache(id, res.val.current, res.val.reset, !res.val.pass); this.metrics.emit({ workspaceId: req.workspaceId, @@ -199,7 +207,7 @@ export class AgentRatelimiter implements RateLimiter { }); } cached.current += cost; - this.setCacheMax(id, cached.current, reset); + this.setCache(id, cached.current, reset, false); return Ok({ pass: true, diff --git a/apps/api/src/routes/legacy_keys_verifyKey.ts b/apps/api/src/routes/legacy_keys_verifyKey.ts index 9ec53f6f95..1cc0523b9b 100644 --- a/apps/api/src/routes/legacy_keys_verifyKey.ts +++ b/apps/api/src/routes/legacy_keys_verifyKey.ts @@ -48,8 +48,7 @@ A key could be invalid for a number of reasons, for example if it has expired, h example: true, }), name: z.string().optional().openapi({ - description: - "The name of the key, give keys a name to easily identifiy their purpose", + description: "The name of the key, give keys a name to easily identify their purpose", example: "Customer X", }), ownerId: z.string().optional().openapi({ diff --git a/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts b/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts index b4a5a9a734..964eaf0caf 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts +++ b/apps/api/src/routes/v1_ratelimit_limit.accuracy.test.ts @@ -96,7 +96,11 @@ for (const { limit, duration, rps, seconds } of testCases) { }, 0); const exactLimit = Math.min(results.length, (limit / (duration / 1000)) * seconds); +<<<<<<< HEAD const upperLimit = Math.round(exactLimit * 2.5); +======= + const upperLimit = Math.round(exactLimit * 1.5); +>>>>>>> 7ffdbc4f (fix: cf cache ratelimits (#2112)) const lowerLimit = Math.round(exactLimit * 0.95); console.info({ name, passed, exactLimit, upperLimit, lowerLimit }); t.expect(passed).toBeGreaterThanOrEqual(lowerLimit); diff --git a/apps/dashboard/CHANGELOG.md b/apps/dashboard/CHANGELOG.md index afa0386d67..5f9e62b6fa 100644 --- a/apps/dashboard/CHANGELOG.md +++ b/apps/dashboard/CHANGELOG.md @@ -1,5 +1,11 @@ # @unkey/web +## 0.1.36 + +### Patch Changes + +- @unkey/ratelimit@0.4.5 + ## 0.1.35 ### Patch Changes diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/keys.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/keys.tsx index c0fb5cd395..c1ea26804c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/keys.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/keys.tsx @@ -98,7 +98,7 @@ export const Keys: React.FC = async ({ keyAuthId, apiId }) => {
{k.name} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index e8102431b0..743a303ce0 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -2,6 +2,7 @@ import { revalidate } from "@/app/actions"; import { CopyButton } from "@/components/dashboard/copy-button"; import { Loading } from "@/components/dashboard/loading"; +import { PageHeader } from "@/components/dashboard/page-header"; import { VisibleButton } from "@/components/dashboard/visible-button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; @@ -77,6 +78,7 @@ const formSchema = z.object({ }, ) .optional(), + recoverEnabled: z.boolean().default(false), limitEnabled: z.boolean().default(false), limit: z .object({ @@ -145,9 +147,10 @@ const formSchema = z.object({ type Props = { apiId: string; keyAuthId: string; + checkStoreEncryptedKeys: boolean; }; -export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { +export const CreateKey: React.FC = ({ apiId, keyAuthId, checkStoreEncryptedKeys }) => { const router = useRouter(); const form = useForm>({ @@ -162,6 +165,7 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { expireEnabled: false, limitEnabled: false, metaEnabled: false, + recoverEnabled: false, ratelimitEnabled: false, }, }); @@ -193,6 +197,9 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { if (!values.ratelimitEnabled) { delete values.ratelimit; } + if (!values.recoverEnabled) { + setRecoverable(false); + } await key.mutateAsync({ keyAuthId, @@ -201,6 +208,7 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { expires: values.expires?.getTime() ?? undefined, ownerId: values.ownerId ?? undefined, remaining: values.limit?.remaining ?? undefined, + recoverEnabled: values.recoverEnabled, enabled: true, }); @@ -219,6 +227,7 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { ? `${split.at(0)}_${"*".repeat(split.at(1)?.length ?? 0)}` : "*".repeat(split.at(0)?.length ?? 0); const [showKey, setShowKey] = useState(false); + const [recoverable, setRecoverable] = useState(false); const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); const resetRateLimit = () => { @@ -247,21 +256,64 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { {key.data ? (
-
-

Your API Key

- -
{key.data.keyId}
- -
-
+

Your API Key

- This key is only shown once and can not be recovered + + {recoverable + ? "This key can be recovered" + : "This key is only shown once and cannot be recovered"} + - Please pass it on to your user or store it somewhere safe. + {recoverable ? ( + <> + It can be recovered using endpoints{" "} + + getKey + {" "} + and{" "} + + listKeys + + . Although we still recommend you to pass it on to your user or store it + somewhere safe. + + ) : ( + "Please pass it on to your user or store it somewhere safe." + )} +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD +

Key ID:

+ +
{key.data.keyId}
+
+ +
+
+

Key:

+======= +>>>>>>> 74903efa (Requested changes) + +======= + +>>>>>>> 7036fdce (merge conflicts) +======= + +======= +>>>>>>> 4b746199 (Error handling for encrypt fail as well as disabled store encrypt keys) +>>>>>>> 984d6938 (Error handling for encrypt fail as well as disabled store encrypt keys)
{showKey ? key.data.key : maskedKey}
@@ -295,6 +347,7 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { form.setValue("expireEnabled", false); form.setValue("ratelimitEnabled", false); form.setValue("metaEnabled", false); + form.setValue("recoverEnabled", false); form.setValue("limitEnabled", false); router.refresh(); }} @@ -809,6 +862,113 @@ export const CreateKey: React.FC = ({ apiId, keyAuthId }) => { ) : null} +<<<<<<< HEAD + {checkStoreEncryptedKeys && ( + + +
+ Recoverable + + ( + + Recoverable + + { + field.onChange(e); + setRecoverable(e); + if (field.value === false) { + resetLimited(); + } + }} + /> + + + )} + /> +
+ + {form.watch("recoverEnabled") ? ( + <> + {form.formState.errors.ratelimit && ( +

+ {form.formState.errors.ratelimit.message} +

+ )} + + ) : null} +

+ You can choose to recover and display plaintext keys later, though it's + not recommended. Recoverable keys are securely stored in an encrypted + vault. For more, visit{" "} + + unkey.com/docs/security/recovering-keys. + +

+
+
+ )} +======= + + +
+ Recoverable + + ( + + Recoverable + + { + field.onChange(e); + if (field.value === false) { + resetLimited(); + } + }} + /> + + + )} + /> +
+ + {form.watch("recoverEnabled") ? ( + <> + {form.formState.errors.ratelimit && ( +

+ {form.formState.errors.ratelimit.message} +

+ )} + + ) : null} +

+ You can choose to recover and display plaintext keys later, though it's + not recommended. Recoverable keys are securely stored in an encrypted +<<<<<<< HEAD + vault. For more, visit unkey.com/docs/security/recovering-keys. +======= + vault. For more, visit{" "} + + unkey.com/docs/security/recovering-keys. + +>>>>>>> 7036fdce (merge conflicts) +

+
+
+>>>>>>> 0ef4416e (Recoverable keys feature for the dashboard)