Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create recoverable keys from dashboard #2783

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ export const dynamic = "force-dynamic";
type Props = {
apiId: string;
keyAuthId: string;
storeEncryptedKeys: boolean;
defaultBytes: number | null;
defaultPrefix: string | null;
};

export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Props) => {
export const CreateKey = ({
apiId,
keyAuthId,
storeEncryptedKeys,
defaultBytes,
defaultPrefix,
}: Props) => {
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: async (data, context, options) => {
Expand All @@ -67,6 +74,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
expireEnabled: false,
limitEnabled: false,
metaEnabled: false,
recoverEnabled: false,
ratelimitEnabled: false,
},
});
Expand Down Expand Up @@ -105,6 +113,9 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
if (refill?.interval === "monthly" && !refill.refillDay) {
refill.refillDay = 1;
}
if (!values.recoverEnabled) {
setRecoverable(false);
}

await key.mutateAsync({
keyAuthId,
Expand All @@ -120,6 +131,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
refillDay: refill.interval === "daily" ? null : refill.refillDay ?? 1,
}
: undefined,
recoverEnabled: values.recoverEnabled,
enabled: true,
});

Expand All @@ -139,6 +151,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
: "*".repeat(split.at(0)?.length ?? 0);
const [showKey, setShowKey] = useState(false);
const [showKeyInSnippet, setShowKeyInSnippet] = useState(false);
const [recoverable, setRecoverable] = useState(false);

const resetRateLimit = () => {
// set them to undefined so the form resets properly.
Expand Down Expand Up @@ -176,9 +189,36 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
</div>
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertTitle>This key is only shown once and can not be recovered </AlertTitle>
<AlertTitle>
{recoverable
? "This key can be recovered"
: "This key is only shown once and cannot be recovered"}
</AlertTitle>
<AlertDescription>
Please pass it on to your user or store it somewhere safe.
{recoverable ? (
<>
It can be recovered using endpoints{" "}
<Link
target="_blank"
href="/docs/api-reference/keys/get"
className="font-medium underline"
>
getKey
</Link>{" "}
and{" "}
<Link
target="_blank"
href="/docs/api-reference/apis/list-keys"
className="font-medium underline"
>
listKeys
</Link>
. 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."
)}
</AlertDescription>
</Alert>
<Code className="flex items-center justify-between w-full gap-4 mt-2 my-8 ph-no-capture max-sm:text-xs sm:overflow-hidden">
Expand Down Expand Up @@ -215,6 +255,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
form.setValue("expireEnabled", false);
form.setValue("ratelimitEnabled", false);
form.setValue("metaEnabled", false);
form.setValue("recoverEnabled", false);
form.setValue("limitEnabled", false);
router.refresh();
}}
Expand Down Expand Up @@ -759,7 +800,57 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro
) : null}
</CardContent>
</Card>
{storeEncryptedKeys && (
<Card>
<CardContent className="justify-between w-full p-4 item-center">
<div className="flex items-center justify-between w-full">
<span>Recoverable</span>

<FormField
control={form.control}
name="recoverEnabled"
render={({ field }) => (
<FormItem>
<FormLabel className="sr-only">Recoverable</FormLabel>
<FormControl>
<Switch
onCheckedChange={(e) => {
field.onChange(e);
setRecoverable(e);
if (field.value === false) {
resetLimited();
}
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>

{form.watch("recoverEnabled") ? (
<>
{form.formState.errors.ratelimit && (
<p className="text-xs text-center text-content-alert">
{form.formState.errors.ratelimit.message}
</p>
)}
</>
) : null}
<p className="text-xs text-content-subtle">
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{" "}
<Link
className="font-semibold"
href={"unkey.com/docs/security/recovering-keys"}
>
unkey.com/docs/security/recovering-keys.
</Link>
</p>
</CardContent>
</Card>
)}
<div className="w-full">
<Button
className="w-full"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default async function CreateKeypage(props: {
<CreateKey
keyAuthId={keyAuth.id}
apiId={props.params.apiId}
storeEncryptedKeys={keyAuth.storeEncryptedKeys}
defaultBytes={keyAuth.defaultBytes}
defaultPrefix={keyAuth.defaultPrefix}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const formSchema = z.object({
.min(new Date(new Date().getTime() + 2 * 60000))
.optional(),
ratelimitEnabled: z.boolean().default(false),
recoverEnabled: z.boolean().default(false),
ratelimit: z
.object({
async: z.boolean().default(false),
Expand Down
36 changes: 35 additions & 1 deletion apps/dashboard/lib/trpc/routers/key/create.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { insertAuditLogs } from "@/lib/audit";
import { db, schema } from "@/lib/db";
import { env } from "@/lib/env";
import { TRPCError } from "@trpc/server";
import { newId } from "@unkey/id";
import { newKey } from "@unkey/keys";
import { type EncryptRequest, type RequestContext, Vault } from "@unkey/vault";
import { z } from "zod";
import { auth, t } from "../../trpc";

Expand Down Expand Up @@ -32,6 +34,7 @@ export const createKey = t.procedure
})
.optional(),
enabled: z.boolean().default(true),
recoverEnabled: z.boolean().optional(),
environment: z.string().optional(),
}),
)
Expand Down Expand Up @@ -108,7 +111,38 @@ export const createKey = t.procedure
enabled: input.enabled,
environment: input.environment,
});

if (input.recoverEnabled && keyAuth.storeEncryptedKeys) {
const vault = new Vault(env().AGENT_URL, env().AGENT_TOKEN);
const encryptReq: EncryptRequest = {
keyring: workspace.id,
data: key,
};
const requestId = crypto.randomUUID();
const context: RequestContext = { requestId };
const vaultRes = await vault.encrypt(context, encryptReq).catch((err) => {
console.error(err);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Encryption Failed. Please contact support using [email protected]",
});
});
await tx
.insert(schema.encryptedKeys)
.values({
keyId: keyId,
workspaceId: workspace.id,
encrypted: vaultRes.encrypted,
encryptionKeyId: vaultRes.keyId,
})
.catch((err) => {
console.error(err);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"We are unable to store encrypted the key. Please contact support using [email protected]",
});
});
}
await insertAuditLogs(tx, {
workspaceId: workspace.id,
actor: { type: "user", id: ctx.user.id },
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@unkey/ui": "workspace:^",
"@unkey/validation": "workspace:^",
"@unkey/vercel": "workspace:^",
"@unkey/vault": "workspace:^",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Workspace pattern doesn't include the vault package location

The build failure is occurring because the vault package is located at internal/vault/ but the workspace configuration in the root package.json only includes "apps/*" and "packages/*" patterns. The vault package needs to be moved to the packages/ directory or the workspace pattern needs to be updated to include the internal/* pattern.

  • Move internal/vault/ to packages/vault/
    OR
  • Add "internal/*" to the workspaces array in the root package.json
🔗 Analysis chain

Verify the workspace dependency setup.

The build failure mentioned in the PR comments suggests that the @unkey/vault module is not being found. Please ensure that:

  1. The vault package exists in the workspace
  2. The vault package is built before the dashboard
  3. Type declarations are properly generated and exported

Let's verify the workspace setup with the following script:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the workspace setup for @unkey/vault

# Check if the vault package exists in the workspace
echo "Checking for vault package..."
fd -t f "package.json" -x grep -l "@unkey/vault"

# Check for type declarations
echo "Checking for type declarations..."
fd -t f "index.d.ts" -x grep -l "vault"

# Check package.json files for build order dependencies
echo "Checking build dependencies..."
fd -t f "package.json" -x grep -l "\"build\".*@unkey/vault"

Length of output: 419


Script:

#!/bin/bash
# Description: Deep dive into vault package setup

# Check vault package.json contents
echo "=== Vault package.json contents ==="
cat ./internal/vault/package.json

# Check for TypeScript configuration
echo -e "\n=== TypeScript configuration files ==="
fd -t f "tsconfig.json|.d.ts$" -p internal/vault

# Check for workspace-level configuration
echo -e "\n=== Workspace configuration ==="
cat package.json

Length of output: 2340

"@vercel/og": "^0.6.2",
"@visx/axis": "^3.10.1",
"@visx/curve": "^3.3.0",
Expand Down
Loading
Loading