diff --git a/docs/app/docs/[[...slug]]/page.tsx b/docs/app/docs/[[...slug]]/page.tsx index dbe5398c..3c7924ca 100644 --- a/docs/app/docs/[[...slug]]/page.tsx +++ b/docs/app/docs/[[...slug]]/page.tsx @@ -21,6 +21,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"; import { contents } from "@/components/sidebar-content"; import { Endpoint } from "@/components/endpoint"; import { DividerText } from "@/components/divider-text"; +import { GenerateAppleJwt } from "@/components/generate-apple-jwt"; const { AutoTypeTable } = createTypeTable(); @@ -85,6 +86,7 @@ export default async function Page({ Tabs, AutoTypeTable, GenerateSecret, + GenerateAppleJwt, AnimatePresence, TypeTable, Features, diff --git a/docs/components/generate-apple-jwt.tsx b/docs/components/generate-apple-jwt.tsx new file mode 100644 index 00000000..9d0aa4c4 --- /dev/null +++ b/docs/components/generate-apple-jwt.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { KJUR } from "jsrsasign"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +// Zod schema for validation +const appleJwtSchema = z.object({ + teamId: z.string().min(1, { message: "Team ID is required." }), + clientId: z + .string() + .min(1, { message: "Client ID (Service ID) is required." }), + keyId: z.string().min(1, { message: "Key ID is required." }), + privateKey: z + .string() + .min(1, { message: "Private Key content is required." }) + .refine( + (key) => key.startsWith("-----BEGIN PRIVATE KEY-----"), + "Private key must be in PKCS#8 PEM format (starting with -----BEGIN PRIVATE KEY-----)", + ) + .refine( + (key) => key.includes("-----END PRIVATE KEY-----"), + "Private key must be in PKCS#8 PEM format (ending with -----END PRIVATE KEY-----)", + ), +}); + +type AppleJwtFormValues = z.infer; + +export const GenerateAppleJwt = () => { + const [generatedJwt, setGeneratedJwt] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(appleJwtSchema), + defaultValues: { + teamId: "", + clientId: "", + keyId: "", + privateKey: "", + }, + }); + + const onSubmit = async (data: AppleJwtFormValues) => { + setIsLoading(true); + setGeneratedJwt(null); + setError(null); + + try { + //normalize the private key by replacing \r\n with \n and trimming whitespace just incase lol + const normalizedKey = data.privateKey.replace(/\r\n/g, "\n").trim(); + + //since jose is not working with safari, we are using jsrsasign + + const header = { + alg: "ES256", + kid: data.keyId, + typ: "JWT", + }; + + const issuedAtSeconds = Math.floor(Date.now() / 1000); + const expirationSeconds = issuedAtSeconds + 180 * 24 * 60 * 60; // 180 days. Should we let the user choose this ? MAX is 6 months + + const payload = { + iss: data.teamId, // Issuer (Team ID) + aud: "https://appleid.apple.com", // Audience + sub: data.clientId, // Subject (Client ID -> Service ID) + iat: issuedAtSeconds, // Issued At timestamp + exp: expirationSeconds, // Expiration timestamp + }; + + const sHeader = JSON.stringify(header); + const sPayload = JSON.stringify(payload); + + const jwt = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, normalizedKey); + setGeneratedJwt(jwt); + } catch (err: any) { + console.error("JWT Generation Error:", err); + setError( + `Failed to generate JWT: ${ + err.message || "Unknown error" + }. Check key format and details.`, + ); + } finally { + setIsLoading(false); + } + }; + + const copyToClipboard = () => { + if (generatedJwt) { + navigator.clipboard.writeText(generatedJwt); + } + }; + + return ( +
+
+ + ( + + Apple Team ID + + + + + + )} + /> + ( + + Client ID (Service ID) + + + + + The identifier for the service you created in Apple Developer. + + + + )} + /> + ( + + Key ID + + + + + The ID associated with your private key (.p8 file). + + + + )} + /> + ( + + Private Key Content (.p8 file content) + +