{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-azure-zod",
  "title": "Azure Provider (Zod)",
  "description": "A self-contained form component that emits an OidcPolicy for a Microsoft Entra ID workload: tenant, the caller's object ID, optional client ID, and audience. Validated with Zod.",
  "dependencies": [
    "zod"
  ],
  "registryDependencies": [
    "https://ui.shadcn.com/oidc/r/policy.json",
    "https://ui.shadcn.com/oidc/r/claims.json",
    "button",
    "field",
    "input"
  ],
  "files": [
    {
      "path": "registry/oidc/flavors/none-zod/provider-azure.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { z } from \"zod\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Field,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n} from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  ClaimChip,\n  ClaimsList,\n  claimRowsFromPolicy,\n  claimsListValid,\n  mergeClaimRows,\n} from \"@/components/oidc/claims\"\nimport type { OidcPolicy } from \"@/lib/oidc/policy\"\n\nfunction AzureIcon({ ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" {...props}>\n      <path d=\"M22.379 23.343a1.62 1.62 0 0 0 1.536-2.14v.002L17.35 1.76A1.62 1.62 0 0 0 15.816.657H8.184A1.62 1.62 0 0 0 6.65 1.76L.086 21.204a1.62 1.62 0 0 0 1.536 2.139h4.741a1.62 1.62 0 0 0 1.535-1.103l.977-2.892 4.947 3.675c.28.208.618.32.966.32m-3.084-12.531 3.624 10.739a.54.54 0 0 1-.51.713v-.001h-.03a.54.54 0 0 1-.322-.106l-9.287-6.9h4.853m6.313 7.006c.116-.326.13-.694.007-1.058L9.79 1.76a1.722 1.722 0 0 0-.007-.02h6.034a.54.54 0 0 1 .512.366l6.562 19.445a.54.54 0 0 1-.338.684\" />\n    </svg>\n  )\n}\n\nconst tenantIdSchema = z\n  .string()\n  .min(1, \"Enter the tenant ID.\")\n  .regex(\n    /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,\n    \"Enter the tenant GUID.\"\n  )\nconst objectIdSchema = z\n  .string()\n  .min(1, \"Enter the object ID.\")\n  .regex(\n    /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,\n    \"Enter the object GUID.\"\n  )\nconst clientIdSchema = z.union([\n  z.literal(\"\"),\n  z\n    .string()\n    .regex(\n      /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,\n      \"Enter the client GUID.\"\n    ),\n])\nconst audienceSchema = z.string().min(1, \"Enter the audience.\")\nconst extraClaimsSchema = z.array(\n  z.object({ name: z.string(), values: z.string() })\n)\n\nconst schema = z.object({\n  tenantId: tenantIdSchema,\n  objectId: objectIdSchema,\n  clientId: clientIdSchema,\n  audience: audienceSchema,\n  extraClaims: extraClaimsSchema,\n})\n\ntype Fields = z.infer<typeof schema>\n\nconst DEFAULTS: Fields = {\n  tenantId: \"\",\n  objectId: \"\",\n  clientId: \"\",\n  audience: \"\",\n  extraClaims: [],\n}\n\nfunction fieldErrors(fields: Fields) {\n  const result = schema.safeParse(fields)\n  const errors: Partial<Record<keyof Fields, string>> = {}\n  if (!result.success) {\n    for (const issue of result.error.issues) {\n      const key = issue.path[0] as keyof Fields | undefined\n      if (key && !errors[key]) {\n        errors[key] = issue.message\n      }\n    }\n  }\n  return { valid: result.success, errors }\n}\n\nconst FIRST_CLASS_CLAIMS = [\"aud\", \"oid\", \"azp\"]\n\nconst ISSUER_PATTERN =\n  /^https:\\/\\/login\\.microsoftonline\\.com\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\/v2\\.0$/\n\nconst GUID_PATTERN =\n  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/\n\n// Entra tokens carry lowercase GUIDs; normalize pastes.\nfunction compile(fields: Fields) {\n  const claims: Record<string, string[]> = {}\n  if (fields.audience) {\n    claims.aud = [fields.audience]\n  }\n  if (fields.objectId) {\n    claims.oid = [fields.objectId.toLowerCase()]\n  }\n  if (fields.clientId) {\n    claims.azp = [fields.clientId.toLowerCase()]\n  }\n  mergeClaimRows(claims, fields.extraClaims, FIRST_CLASS_CLAIMS)\n  return {\n    issuer: `https://login.microsoftonline.com/${fields.tenantId.toLowerCase()}/v2.0`,\n    claims,\n  }\n}\n\nfunction parse(policy: OidcPolicy) {\n  const tenantId = ISSUER_PATTERN.exec(policy.issuer)?.[1]\n  if (!tenantId) {\n    return null\n  }\n  if (\n    FIRST_CLASS_CLAIMS.some((name) => (policy.claims[name]?.length ?? 0) > 1)\n  ) {\n    return null\n  }\n  const objectId = policy.claims.oid?.[0] ?? \"\"\n  const clientId = policy.claims.azp?.[0] ?? \"\"\n  if (\n    (objectId && !GUID_PATTERN.test(objectId)) ||\n    (clientId && !GUID_PATTERN.test(clientId))\n  ) {\n    return null\n  }\n  const extraClaims = claimRowsFromPolicy(policy.claims, FIRST_CLASS_CLAIMS)\n  if (extraClaims === null) {\n    return null\n  }\n\n  return {\n    tenantId,\n    objectId,\n    clientId,\n    audience: policy.claims.aud?.[0] ?? \"\",\n    extraClaims,\n  }\n}\n\n/** True when this form can represent the policy — for routing an edit. */\nfunction matchesAzurePolicy(policy: OidcPolicy) {\n  return parse(policy) !== null\n}\n\nfunction AzurePolicyForm({\n  defaultValue,\n  onChange,\n  onSubmit,\n  className,\n}: {\n  defaultValue?: OidcPolicy\n  onChange?: (policy: OidcPolicy) => void\n  onSubmit: (policy: OidcPolicy) => void\n  className?: string\n}) {\n  const [fields, setFields] = React.useState<Fields>(() =>\n    defaultValue ? (parse(defaultValue) ?? DEFAULTS) : DEFAULTS\n  )\n  const [attempted, setAttempted] = React.useState(false)\n  const { errors } = fieldErrors(fields)\n\n  const apply = (next: Fields) => {\n    setFields(next)\n    const policy = compile(next)\n    onChange?.(\n      defaultValue?.label ? { ...policy, label: defaultValue.label } : policy\n    )\n  }\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    if (!(\n      fieldErrors(fields).valid &&\n      claimsListValid(fields.extraClaims, FIRST_CLASS_CLAIMS)\n    )) {\n      setAttempted(true)\n      return\n    }\n    const policy = compile(fields)\n    onSubmit(\n      defaultValue?.label ? { ...policy, label: defaultValue.label } : policy\n    )\n  }\n\n  return (\n    <form\n      data-slot=\"azure-policy-form\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      onSubmit={handleSubmit}\n    >\n      <Field\n        data-invalid={!!(errors.tenantId && (fields.tenantId || attempted))}\n      >\n        <FieldLabel htmlFor=\"az-tenantId\" className=\"w-full\">\n          Tenant ID <ClaimChip>iss</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"az-tenantId\"\n          value={fields.tenantId}\n          placeholder=\"aaaabbbb-0000-cccc-1111-dddd2222eeee\"\n          className=\"font-mono\"\n          aria-invalid={!!(errors.tenantId && (fields.tenantId || attempted))}\n          onChange={(event) =>\n            apply({ ...fields, tenantId: event.target.value.trim() })\n          }\n        />\n        {!!(errors.tenantId && (fields.tenantId || attempted)) ? (\n          <FieldError>{errors.tenantId}</FieldError>\n        ) : null}\n        <FieldDescription>\n          Entra admin center → Overview → Tenant ID. Sets the issuer\n          https://login.microsoftonline.com/&lt;tenant&gt;/v2.0.\n        </FieldDescription>\n      </Field>\n      <Field\n        data-invalid={!!(errors.objectId && (fields.objectId || attempted))}\n      >\n        <FieldLabel htmlFor=\"az-objectId\" className=\"w-full\">\n          Object ID <ClaimChip>oid</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"az-objectId\"\n          value={fields.objectId}\n          placeholder=\"aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb\"\n          className=\"font-mono\"\n          aria-invalid={!!(errors.objectId && (fields.objectId || attempted))}\n          onChange={(event) =>\n            apply({ ...fields, objectId: event.target.value.trim() })\n          }\n        />\n        {!!(errors.objectId && (fields.objectId || attempted)) ? (\n          <FieldError>{errors.objectId}</FieldError>\n        ) : null}\n        <FieldDescription>\n          The workload&apos;s Object (principal) ID. Survives renames.\n        </FieldDescription>\n      </Field>\n      <Field\n        data-invalid={!!(errors.clientId && (fields.clientId || attempted))}\n      >\n        <FieldLabel htmlFor=\"az-clientId\" className=\"w-full\">\n          Application (client) ID <ClaimChip>azp</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"az-clientId\"\n          value={fields.clientId}\n          placeholder=\"00001111-aaaa-2222-bbbb-3333cccc4444\"\n          className=\"font-mono\"\n          aria-invalid={!!(errors.clientId && (fields.clientId || attempted))}\n          onChange={(event) =>\n            apply({ ...fields, clientId: event.target.value.trim() })\n          }\n        />\n        {!!(errors.clientId && (fields.clientId || attempted)) ? (\n          <FieldError>{errors.clientId}</FieldError>\n        ) : null}\n        <FieldDescription>\n          Optional: also pin the app registration the token was issued to.\n        </FieldDescription>\n      </Field>\n      <Field\n        data-invalid={!!(errors.audience && (fields.audience || attempted))}\n      >\n        <FieldLabel htmlFor=\"az-audience\" className=\"w-full\">\n          Audience <ClaimChip>aud</ClaimChip>\n        </FieldLabel>\n        <Input\n          id=\"az-audience\"\n          value={fields.audience}\n          placeholder=\"api://your-app\"\n          aria-invalid={!!(errors.audience && (fields.audience || attempted))}\n          onChange={(event) =>\n            apply({ ...fields, audience: event.target.value.trim() })\n          }\n        />\n        {!!(errors.audience && (fields.audience || attempted)) ? (\n          <FieldError>{errors.audience}</FieldError>\n        ) : null}\n        <FieldDescription>\n          The audience the caller requests its token for.\n        </FieldDescription>\n      </Field>\n      <ClaimsList\n        value={fields.extraClaims}\n        onChange={(rows) => apply({ ...fields, extraClaims: rows })}\n        reserved={FIRST_CLASS_CLAIMS}\n      />\n      <Button type=\"submit\" className=\"self-start\">\n        Save\n      </Button>\n    </form>\n  )\n}\n\nexport { AzureIcon, AzurePolicyForm, matchesAzurePolicy }\n",
      "type": "registry:component",
      "target": "@components/oidc/provider-azure.tsx"
    }
  ],
  "meta": {
    "tagline": "Trust an Entra ID workload"
  },
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}